package com.nulldev.util.web.HttpServer3;

import static com.nulldev.util.web.HttpServer3.util.HttpServerUtil.ALPN_HTTP_1_ONLY;
import static com.nulldev.util.web.HttpServer3.util.HttpServerUtil.HARDCODED_ERROR;
import static com.nulldev.util.web.HttpServer3.util.HttpServerUtil.HARDCODED_ICE_ERROR;
import static com.nulldev.util.web.HttpServer3.util.HttpServerUtil.KEEP_ALIVE_MAX_CONNS;
import static com.nulldev.util.web.HttpServer3.util.HttpServerUtil.KEEP_ALIVE_TIMEOUT;
import static com.nulldev.util.web.HttpServer3.util.HttpServerUtil.SO_LINGER_VAL;
import static com.nulldev.util.web.HttpServer3.util.HttpServerUtil.SO_TIMEOUT_VAL;
import static com.nulldev.util.web.HttpServer3.util.HttpServerUtil.__apply_keep_alive_workarounds;
import static com.nulldev.util.web.HttpServer3.util.HttpServerUtil.__apply_settings_to_server_socket;
import static com.nulldev.util.web.HttpServer3.util.HttpServerUtil.__check_and_set_keepalive;
import static com.nulldev.util.web.HttpServer3.util.HttpServerUtil.__finish_stream;
import static com.nulldev.util.web.HttpServer3.util.HttpServerUtil.__get_pw;
import static com.nulldev.util.web.HttpServer3.util.HttpServerUtil.__guess_content_length;
import static com.nulldev.util.web.HttpServer3.util.HttpServerUtil.__has_header;
import static com.nulldev.util.web.HttpServer3.util.HttpServerUtil.__sock_SO_SNDBUF;
import static com.nulldev.util.web.HttpServer3.util.HttpServerUtil.generateStandardSocket;
import static com.nulldev.util.web.HttpServer3.util.HttpServerUtil.getTLSVersion;
import static com.nulldev.util.web.HttpServer3.util.HttpServerUtil.hasBlankResponse;
import static com.nulldev.util.web.HttpServer3.util.HttpServerUtil.isIgnorableException;
import static com.nulldev.util.web.HttpServer3.util.HttpServerUtil.isSocketClosed;
import static com.nulldev.util.web.HttpServer3.util.HttpServerUtil.stringForResponse;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.SequenceInputStream;
import java.net.ConnectException;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.ServerSocket;
import java.net.Socket;
import java.net.SocketException;
import java.net.SocketTimeoutException;
import java.net.URLConnection;
import java.nio.channels.WritableByteChannel;
import java.time.Duration;
import java.util.Map;
import java.util.TreeMap;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.LongAdder;
import java.util.regex.Pattern;
import java.util.zip.Deflater;
import java.util.zip.DeflaterOutputStream;
import java.util.zip.GZIPOutputStream;

import javax.net.ssl.KeyManager;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLException;
import javax.net.ssl.SSLHandshakeException;
import javax.net.ssl.SSLParameters;
import javax.net.ssl.SSLServerSocket;
import javax.net.ssl.SSLServerSocketFactory;
import javax.net.ssl.SSLSocket;
import javax.net.ssl.SSLSocketFactory;
import javax.net.ssl.TrustManager;
import javax.net.ssl.X509TrustManager;

import com.nulldev.util.JVM.Arguments;
import com.nulldev.util.JVM.JVM;
import com.nulldev.util.VariableAPI.ArrayUtils;
import com.nulldev.util.VariableAPI.ArrayUtils.StringArrayUtils;
import com.nulldev.util.VariableAPI.MathUtil;
import com.nulldev.util.VariableAPI.StringsUtil;
import com.nulldev.util.VariableAPI.util.strings.splitters.Splitters;
import com.nulldev.util.VariableAPI.util.strings.splitters.third_party.Splitter;
import com.nulldev.util.concurrency.threadIt.v4.asyncTasks.AsyncTaskCallback;
import com.nulldev.util.concurrency.threadIt.v4.emapi.ExecutorManager;
import com.nulldev.util.concurrency.threadIt.v4.emapi.IExecutorManager;
import com.nulldev.util.concurrency.threadIt.v4.emapi.cntft.ControllableFutureTask;
import com.nulldev.util.concurrency.threadIt.v4.emapi.uft.Executable;
import com.nulldev.util.concurrency.threadIt.v4.emapi.uft.Returnable;
import com.nulldev.util.concurrency.threadIt.v4.executors.bqde.BQDE;
import com.nulldev.util.data.Variables;
import com.nulldev.util.data.variable;
import com.nulldev.util.data.Arrays.maps.FastHashMap;
import com.nulldev.util.data.Arrays.maps.FastMaps;
import com.nulldev.util.data.Arrays.maps.trie.TrieMap;
import com.nulldev.util.data.Charsets.CharsetUtil;
import com.nulldev.util.data.CompressionAPI.thirdparty.airlift.snappy.SnappyFramedOutputStream;
import com.nulldev.util.data.CompressionAPI.thirdparty.airlift.zstd.ZstdOutputStream;
import com.nulldev.util.data.CompressionAPI.thirdparty.lzc.LZCOutputStream;
import com.nulldev.util.data.CompressionAPI.thirdparty.mig.MiGzOutputStream;
import com.nulldev.util.data.CompressionAPI.thirdparty.pgzip.ParallelGZIPOutputStream;
import com.nulldev.util.data.Sets.DataTriplet;
import com.nulldev.util.io.IOUtils;
import com.nulldev.util.io.extensions.copyable.Copyable;
import com.nulldev.util.io.extensions.copyable.nio.NIOCopyable;
import com.nulldev.util.io.file.FileUtil;
import com.nulldev.util.io.legacy.MimeType;
import com.nulldev.util.io.throttling.BandwidthThrottledOutputStream;
import com.nulldev.util.io.util.TransferToBackport;
import com.nulldev.util.logging.Logger;
import com.nulldev.util.logging.LoggerFactory;
import com.nulldev.util.manage.Macros;
import com.nulldev.util.manage.Version;
import com.nulldev.util.networking.ip.IP;
import com.nulldev.util.networking.misc.tls.TLSStandardFiltering;
import com.nulldev.util.web.HttpClient.HttpClient;
import com.nulldev.util.web.HttpClient.HttpResponse;
import com.nulldev.util.web.HttpServer3.HttpServer3Settings.HTTPSUpgradePolicy;
import com.nulldev.util.web.HttpServer3.MicroCache.CachedPage;
import com.nulldev.util.web.HttpServer3.extensions.botAway.BotAway;
import com.nulldev.util.web.HttpServer3.extensions.crawlerBlock.CrawlerBlock;
import com.nulldev.util.web.HttpServer3.extensions.envStats.TCPDebugPage;
import com.nulldev.util.web.HttpServer3.extensions.fakeFlare.CDNCGITracePage;
import com.nulldev.util.web.HttpServer3.extensions.http2.api_compat.HttpRequestDispatcher;
import com.nulldev.util.web.HttpServer3.extensions.http2.compat.HttpRequestValidator;
import com.nulldev.util.web.HttpServer3.extensions.http2.compat.IOContainer;
import com.nulldev.util.web.HttpServer3.extensions.http2.compat.RequestValidatorConfig;
import com.nulldev.util.web.HttpServer3.extensions.http2.util.ServerThreadUtils;
import com.nulldev.util.web.HttpServer3.extensions.nCaptcha.nCaptcha;
import com.nulldev.util.web.HttpServer3.extensions.nCaptcha.backend.util.UUIDCookieLoader;
import com.nulldev.util.web.HttpServer3.extensions.sse.SSEPage;
import com.nulldev.util.web.HttpServer3.extensions.sse.SSESession;
import com.nulldev.util.web.HttpServer3.extensions.websocket.IWebSocketPage;
import com.nulldev.util.web.HttpServer3.extensions.websocket.Peer;
import com.nulldev.util.web.HttpServer3.extensions.websocket.ProtocolUtils;
import com.nulldev.util.web.HttpServer3.extensions.websocket.util.CloseReason;
import com.nulldev.util.web.HttpServer3.legacyPages.v1.HttpPage;
import com.nulldev.util.web.HttpServer3.legacyPages.v1.RequestType;
import com.nulldev.util.web.HttpServer3.legacyPages.v2.HttpPage2;
import com.nulldev.util.web.HttpServer3.pages.v3.ASyncHttpPage3;
import com.nulldev.util.web.HttpServer3.pages.v3.HttpPage3;
import com.nulldev.util.web.HttpServer3.pages.v3.HttpPage3.CachingMethod;
import com.nulldev.util.web.HttpServer3.pages.v3.HttpPage3.EnhancedSecurityChecks;
import com.nulldev.util.web.HttpServer3.pages.v3.HttpPageRequestValidator;
import com.nulldev.util.web.HttpServer3.pages.v3.RequestProtocolType;
import com.nulldev.util.web.HttpServer3.util.BoundedInputStream;
import com.nulldev.util.web.HttpServer3.util.ChunkedOutputStream;
import com.nulldev.util.web.HttpServer3.util.HTTPPrintWriter;
import com.nulldev.util.web.HttpServer3.util.HttpServerUtil.HTTPRequestPhase;
import com.nulldev.util.web.HttpServer3.util.ProxyBotStop;
import com.nulldev.util.web.HttpServer3.util.SectionedInputStream;

/**
 * A continuation and improvement of HttpServer 2. <br>
 * 
 * HttpServer, a lightweight and fast web server that is simple to use and
 * quick.
 * 
 * New Features: <br>
 * <ul>
 * <li>Body data is now accquired correctly. (FIXED)</li>
 * <li>Internal Anti DDOS/DOS system. (rate-limiter) [work-in-progress]</li>
 * <li>Micro caching (>8MB on systems above 128MB) [done]</li>
 * <li>Improved HTTPS support [done]</li>
 * <li>(POSSIBLE) HTTP/2.0 support</li>
 * <li>iWebKit's STTP support (Secure [Hyper] Text Transfer Protocol) [wip]</li>
 * </ul>
 * 
 * API Changes: <br>
 * <ul>
 * <li>Removed overrideResponse. (known as "override" in 2.x)</li>
 * <li>Constructor has been changed to accept KeyManagers for SSL support.</li>
 * <li>The flags and settings object is still incomplete.</li>
 * <li>Removed most iWebKit references.</li>
 * <li>Fixed HttpPage2's re-implementation.</li>
 * </ul>
 * 
 * @author null
 * @since 3.0.0 (1.0 was implemented in 2.5, 2.0 in 2.6, 2.0.1 in 2.6.2, 2.1 in
 *        2.7)
 */
public class HttpServer3 implements HttpServer {
	/**
	 * Ongoing work: <br>
	 * - Timeout and dead request fixes. (PHASE 2) <br>
	 * - Move getTLSVersion(), generateSSL(KeyManager[]),
	 * generateSSLSocket(KeyManager[]) and generateStandardSocket() into a separate
	 * utility class.
	 *
	 * See the CHANGELOG file for changes.
	 *
	 * Planned work: <br>
	 * - Reattempt HTTP/2.0. <br>
	 * - Merge libSocketReader patchset, once SSL is compliant via NIO. <br>
	 * - Update the timingDebug to include basic URL indicators.
	 */
	private static final Logger log = LoggerFactory.getLoggerDT(Arguments.hasArgument("--HttpServer3.timingDebug"));
	private static final Splitter QUERY_SPLIT = Splitter.onPattern("\\?|\\&");

	/* */

	public static final Version VERSION = new Version(3, 0, 6);

	public static final long UNLIMITED_CONNECTIONS = -1;

	public static enum nCaptchaEnforcementMethod {
		PRE_CONNECT,
		NO_ENFORCEMENT
	}

	// END

	public boolean isTimingDebugEnabled = Arguments.hasArgument("--HttpServer3.timingDebug");

	private final HttpServer3Settings settings = new HttpServer3Settings();
	private final HttpServer3Flags flags = new HttpServer3Flags();

	private final IExecutorManager td = Arguments.hasArgument("--HttpServer3.useGlobalExecutor") ? ExecutorManager.get()
			: new ExecutorManager(new BQDE(Runtime.getRuntime().availableProcessors() * 24, 4));

	private final Map<String, Page> PAGES = (this.flags.use_trie_map ? new TrieMap<String, Page>() : new FastHashMap<String, Page>(true));
	private final Map<String, String> ERROR_PAGES = (this.flags.use_trie_map ? new TrieMap<String, String>() : new FastHashMap<String, String>(true));

	private long connectionsLimit = UNLIMITED_CONNECTIONS;

	private final LongAdder connections = new LongAdder();

	private final MicroCache cache;

	private ServerSocket httpServerSocket;
	private SSLServerSocket httpsServerSocket;
	private SSLSocketFactory fact;
	private SSLParameters default_params;

	private SSLServerSocket generateSSLSocket(final KeyManager[] km) throws Exception {
		final SSLContext context = SSLContext.getInstance(getTLSVersion());
		final TrustManager[] trustAllCerts = new TrustManager[]
			{ new X509TrustManager() {
				@Override
				public void checkClientTrusted(final java.security.cert.X509Certificate[] certs, final String authType) {
				}

				@Override
				public void checkServerTrusted(final java.security.cert.X509Certificate[] certs, final String authType) {
				}

				@Override
				public java.security.cert.X509Certificate[] getAcceptedIssuers() {
					return null;
				}
			} };
		context.init(km, trustAllCerts, null);
		final SSLParameters params = context.getDefaultSSLParameters();
		params.setEndpointIdentificationAlgorithm("HTTPS");
		params.setUseCipherSuitesOrder(true);
		params.setProtocols(TLSStandardFiltering.filterProtocols(params.getProtocols()));
		params.setCipherSuites(TLSStandardFiltering.filterCiphers(params.getCipherSuites()));
		if (JVM.getFullVersion().newerThan(1, 8, 251))
			params.setApplicationProtocols(ALPN_HTTP_1_ONLY);
		this.default_params = params;
		final SSLServerSocketFactory ssl = (SSLServerSocketFactory) context.getServerSocketFactory();
		final SSLSocketFactory ssf = context.getSocketFactory();
		this.fact = ssf;
		final SSLServerSocket socket = (SSLServerSocket) ssl.createServerSocket();
		socket.setSSLParameters(params);
		try {
			__apply_settings_to_server_socket(socket, this);
		} catch (SocketException ex) {
			log.error("Failed to apply server socket settings!", ex);
		}
		return socket;
	}

	private InetAddress addr;
	private int port;
	private int httpsPort = 443;

	private int __get_port_http(final SSLBehaviour behaviour, final int defaultPort) {
		switch (Variables.requireNonNullObject(behaviour, "behaviour")) {
			case ATTEMPT_STANDARD:
				return 80;
			case LEGACY_OPTION:
				return -1;
			case ATTEMPT_STANDARD_HTTPS_ONLY:
			default:
				return defaultPort;
		}
	}

	private int __get_port_https(final SSLBehaviour behaviour, final int defaultPort) {
		switch (Variables.requireNonNullObject(behaviour, "behaviour")) {
			case ATTEMPT_STANDARD:
			case ATTEMPT_STANDARD_HTTPS_ONLY:
				return 443;
			case USE_NEAREST_PORT_POSITIVE:
				return IP.freePort(defaultPort + 1, defaultPort + 199);
			case USE_NEAREST_PORT_NEGATIVE:
				return IP.freePort(defaultPort - 199, defaultPort - 1);
			case DISABLED:
				return -1;
			case LEGACY_OPTION:
				return defaultPort;
			default:
				throw new UnsupportedOperationException("Unknown behaviour: " + behaviour);
		}
	}

	/**
	 * HttpServer3 constructor.
	 * 
	 * @param defaultPort (default port for HTTP then +- for HTTPS)
	 * @param localHost   (set to true if you want to test the server)
	 * @param ssl         (enables separate SSL port, requires a KeyManager)
	 * @param km          (can be <code>null</code> if HTTPS/SSL is not needed)
	 * @throws Exception
	 */
	public HttpServer3(final int defaultPort, final boolean localHost, final SSLBehaviour ssl, final KeyManager[] km) throws Exception {
		this(defaultPort, localHost, ssl, km, MicroCache.DEFAULT_SIZE);
	}

	/**
	 * HttpServer3 constructor.
	 * 
	 * @param defaultPort (default port for HTTP then +- for HTTPS)
	 * @param localHost   (set to true if you want to test the server)
	 * @param ssl         (enables separate SSL port, requires a KeyManager)
	 * @param km          (can be <code>null</code> if HTTPS/SSL is not needed)
	 * @throws Exception
	 */
	public HttpServer3(final int defaultPort, final boolean localHost, final SSLBehaviour ssl, final KeyManager[] km, final int cacheSize) throws Exception {
		this(defaultPort,
				(localHost ? IP.localhost()
						: (Arguments.hasArgument("--HttpServer3.bindToAll", "--NTubeServer.bindToAll") ? IP.allAddresses() : IP.lanAddress())),
				ssl, km, cacheSize);
	}

	/**
	 * HttpServer3 constructor.
	 * 
	 * @param defaultPort (default port for HTTP then +- for HTTPS)
	 * @param addr        (IP address to bind to)
	 * @param ssl         (enables separate SSL port, requires a KeyManager)
	 * @param km          (can be <code>null</code> if HTTPS/SSL is not needed)
	 * @throws Exception
	 */
	public HttpServer3(final int defaultPort, final InetAddress addr, final SSLBehaviour ssl, final KeyManager[] km, final int cacheSize) throws Exception {
		this.cache = new MicroCache(cacheSize);
		this.addr = Variables.requireNonNullObject(addr, "addr");
		this.port = __get_port_http(ssl, defaultPort);
		this.httpsPort = __get_port_https(ssl, defaultPort);
		if (Arguments.hasArgument("--HttpServer3.enableCaptcha")) {
			log.info("Enabled captcha.");
			nCaptcha.loadCaptchaSystem(this);
			this.settings.enableNCaptcha = true;
			/* Required for v4. */
			this.flags.enable_ws = true;
		}
		new Thread(new Runnable() {

			@Override
			public void run() {
				try {
					if (HttpServer3.this.port == -1)
						return;
					HttpServer3.this.httpServerSocket = generateStandardSocket();
					try {
						__apply_settings_to_server_socket(HttpServer3.this.httpServerSocket, HttpServer3.this);
					} catch (Exception ex) {
						log.error("Failed to apply server socket settings!", ex);
					}
					HttpServer3.this.httpServerSocket.bind(new InetSocketAddress(HttpServer3.this.addr, HttpServer3.this.port), 1024);
					log.info("Server is now exposed. [ip=" + HttpServer3.this.addr + ",port=" + HttpServer3.this.port + ",ssl=" + ssl + "]");
					while (true) {
						try {
							final Socket s = HttpServer3.this.httpServerSocket.accept();
							// TODO: Should there be a threshold at which we completely reject new
							// connections? (as in not even open and check if we can squeeze a user in)
							HttpServer3.this.td.tryAutoAsyncSubmit(HttpServer3.this.flags.use_async_submissions, () -> {
								try {
									if (!s.isClosed()) {
										try {
											s.setTcpNoDelay(!HttpServer3.this.flags.force_disable_nodelay);
											if (HttpServer3.this.flags.tcp_keep_alive) {
												s.setKeepAlive(true);
											}
											if (HttpServer3.this.settings.enableAddressReuse) {
												s.setReuseAddress(true);
											}
											if (HttpServer3.this.settings.enableSoLinger) {
												s.setSoLinger(true, SO_LINGER_VAL);
											} else {
												s.setSoLinger(false, -1);
											}
											if (HttpServer3.this.settings.enableTimeoutEnforcement) {
												s.setSoTimeout(SO_TIMEOUT_VAL);
											}
											if (HttpServer3.this.flags.sockets_set_throughput) {
												s.setTrafficClass(0x08);
											}
										} catch (Exception e) {
											log.dwarn("Failed to apply socket settings! [e=" + e.getMessage() + "]");
										}
									}
									HttpServer3.this.initialHandle(s, false);
								} catch (Exception e) {
									if (!(e instanceof SSLHandshakeException || e instanceof SSLException)
											&& !(e.getMessage() != null && e.getMessage().equalsIgnoreCase("Connection reset"))) {
										log.error("Handle failed!", e);
										IOUtils.closeQuietly(s);
										return s;
									}
								}
								return s;
							}).addCallbackTask(new AsyncTaskCallback<Socket>() {

								@Override
								public void onCompletion(final Socket result) {
									HttpServer3.this.connections.decrement();
								}

								@Override
								public void onError(Throwable e) {
									HttpServer3.this.connections.decrement();
									if (e instanceof SocketException && (e.getMessage().contains("Broken pipe (Write failed)")
											|| e.getMessage().equalsIgnoreCase("Socket Closed") || e.getMessage().equals("Socket is closed"))) {
										/* Ignored. */
									} else if (e instanceof SSLHandshakeException) {
										if (e.getMessage().endsWith("certificate_unknown") || e.getMessage().endsWith("Broken pipe (Write failed)")) {
											// Literally no-one cares.
										} else {
											log.error("SSL: Error reached!", e.getMessage());
										}
									} else {
										log.error(e);
									}
									IOUtils.closeQuietly(s);
								}

								@Override
								public void onSubmissionRejection() {
									/* We're too busy doing things, abandon ship! */
									IOUtils.closeQuietly(s);
								}
							});
							HttpServer3.this.connections.increment();
						} catch (Exception e) {
							if (!(e instanceof SocketTimeoutException))
								log.error(e);
						}
					}
				} catch (Exception e) {
					throw new RuntimeException(e);
				}
			}
		}, "HS3-HTTP").start();
		new Thread(new Runnable() {

			@Override
			public void run() {
				try {
					if (HttpServer3.this.httpsPort == -1)
						return;
					if (km != null) {
						HttpServer3.this.httpsServerSocket = HttpServer3.this.generateSSLSocket(km);
					}
					if (HttpServer3.this.httpsServerSocket != null) {
						HttpServer3.this.httpsServerSocket.bind(new InetSocketAddress(HttpServer3.this.addr, HttpServer3.this.httpsPort), 1024);
						log.info("HTTPS Server is now exposed. [ip=" + HttpServer3.this.addr + ",port=" + HttpServer3.this.httpsPort + ",ssl=" + ssl + "]");
					}
					if (HttpServer3.this.httpsServerSocket != null) {
						while (true) {
							try {
								final Socket s = HttpServer3.this.httpsServerSocket.accept();
								// TODO: Should there be a threshold at which we completely reject new
								// connections? (as in not even open and check if we can squeeze a user in)
								HttpServer3.this.td.tryAutoAsyncSubmit(HttpServer3.this.flags.use_async_submissions, () -> {
									try {
										if (!s.isClosed()) {
											try {
												s.setTcpNoDelay(!HttpServer3.this.flags.force_disable_nodelay);
												if (HttpServer3.this.flags.tcp_keep_alive) {
													s.setKeepAlive(true);
												}
												if (HttpServer3.this.settings.enableAddressReuse) {
													s.setReuseAddress(true);
												}
												if (HttpServer3.this.settings.enableSoLinger) {
													s.setSoLinger(true, SO_LINGER_VAL);
												} else {
													s.setSoLinger(false, -1);
												}
												if (HttpServer3.this.settings.enableTimeoutEnforcement) {
													s.setSoTimeout(SO_TIMEOUT_VAL);
												}
												if (HttpServer3.this.flags.sockets_set_throughput) {
													s.setTrafficClass(0x08);
												}
											} catch (Exception e) {
												log.dwarn("Failed to apply socket settings! [e=" + e.getMessage() + "]");
											}
										}
										HttpServer3.this.initialHandle(s, true);
									} catch (Exception e) {
										if (!(e instanceof SSLHandshakeException || e instanceof SSLException)) {
											log.error("Handle failed!", e);
											IOUtils.closeQuietly(s);
											return s;
										}
									}
									return s;
								}).addCallbackTask(new AsyncTaskCallback<Socket>() {

									@Override
									public void onCompletion(Socket result) {
										HttpServer3.this.connections.decrement();
									}

									@Override
									public void onError(Throwable e) {
										HttpServer3.this.connections.decrement();
										if (e instanceof SocketException && (e.getMessage().contains("Broken pipe (Write failed)")
												|| e.getMessage().equalsIgnoreCase("Socket Closed") || e.getMessage().equals("Socket is closed"))) {
											/* Ignored. */
										} else if (e instanceof SSLHandshakeException) {
											if (e.getMessage().endsWith("certificate_unknown") || e.getMessage().endsWith("Broken pipe (Write failed)")) {
												// Literally no-one cares.
											} else {
												log.error("SSL: Error reached!", e.getMessage());
											}
										} else {
											log.error(e);
										}
										IOUtils.closeQuietly(s);
									}

									@Override
									public void onSubmissionRejection() {
										/* We're too busy doing things, abandon ship! */
										IOUtils.closeQuietly(s);
									}
								});
								HttpServer3.this.connections.increment();
							} catch (Exception e) {
								if (!(e instanceof SocketTimeoutException))
									log.error(e);
							}
						}
					}
				} catch (Exception e) {
					throw new RuntimeException(e);
				}
			}
		}, "HS3-HTTPS").start();
		this.ERROR_PAGES.put("PAGE_NOT_FOUND", HttpServerErrorStates.PAGE_NOT_FOUND);
		this.ERROR_PAGES.put("INTERNAL_ERROR", HttpServerErrorStates.INTERNAL_ERROR);
		this.ERROR_PAGES.put("SERVER_BUSY", HttpServerErrorStates.SERVER_BUSY);
		this.ERROR_PAGES.put("SECURITY_CHECK_FAIL", HttpServerErrorStates.SECURITY_CHECK_FAIL);
		this.ERROR_PAGES.put("INVALID_REQUEST", HttpServerErrorStates.INVALID_REQUEST);
		this.ERROR_PAGES.put("SERVICE_UNAVAILABLE", HttpServerErrorStates.SERVICE_UNAVAILABLE);
		this.ERROR_PAGES.put("BAD_RESPONSE", HttpServerErrorStates.BAD_RESPONSE);
	}

	public void setServerName(final String value) {
		this.settings.setServerName(value);
	}

	public Page getPage(final String id, final Object sock) {
		if (id == null)
			return null;
		final Page initial = this.PAGES.get(id);
		return (initial != null ? initial : unknown(id, sock));
	}

	public Page unknown(final String id, final Object sock) {
		return null;
	}

	public String getPageNotFound() {
		return this.ERROR_PAGES.get("PAGE_NOT_FOUND");
	}

	public void setPageNotFound(final String pnf) {
		if (pnf == null || pnf.isEmpty()) {
			this.ERROR_PAGES.put("PAGE_NOT_FOUND", HttpServerErrorStates.PAGE_NOT_FOUND);
		} else {
			this.ERROR_PAGES.put("PAGE_NOT_FOUND", pnf);
		}
	}

	public String getInternalErrorPage() {
		return this.ERROR_PAGES.get("INTERNAL_ERROR");
	}

	public void setInternalErrorPage(final String iep) {
		if (iep == null || iep.isEmpty()) {
			this.ERROR_PAGES.put("INTERNAL_ERROR", HttpServerErrorStates.INTERNAL_ERROR);
		} else {
			this.ERROR_PAGES.put("INTERNAL_ERROR", iep);
		}
	}

	public InetAddress getAddress() {
		return this.addr;
	}

	public int getPort() {
		return this.port;
	}

	public int getHTTPSPort() {
		return this.httpsPort;
	}

	private static final String[] REGULAR_REQUEST_ENTRIES = new String[]
		{ "GET ", "POST", "OPTI", "PUT ", "DELE", "HEAD", "TRAC", "PATC", "CONN", "keep", "PRI " };

	private static boolean isRegularRequest(final String p) {
		return ArrayUtils.StringArrayUtils.containsStartsWith(REGULAR_REQUEST_ENTRIES, p);
	}

	private boolean isTooManyConnections() {
		if (!this.settings.enableNoDDOS)
			return false;
		else if (this.connectionsLimit <= 0)
			return false;
		else
			return this.connections.sum() >= (this.settings.enableNoDDOS ? NO_DDOS_CONNECTION_LIMIT : this.connectionsLimit);
	}

	private void __TIMING(final String str, final long stamp) {
		// XXX: Roll this out to the rest of HttpServer3.
		if (this.isTimingDebugEnabled)
			log.debug(str + " " + (System.currentTimeMillis() - stamp) + " ms");
	}

	private static final int NO_DDOS_CONNECTION_LIMIT = 4096; // 10240
	private static final int REGULAR_REQUEST_DETECTION_SIZE = 4;

	private void initialHandle(final Socket socket, boolean expectHTTPS) throws Exception {
		final long start = this.isTimingDebugEnabled ? System.currentTimeMillis() : 0;
		__TIMING("actualhandle::start:", start);
		if (this.isTooManyConnections()) {
			if (this.connections.sum() + 32 >= (this.settings.enableNoDDOS ? NO_DDOS_CONNECTION_LIMIT : this.connectionsLimit) || expectHTTPS) {
				// Too many connections, just refuse the user.
				// Using TLS/SSL at this point would be too much extra load, bail instead.
				IOUtils.closeQuietly(socket);
				return;
			} else {
				log.debug("[invoke_error] Cause: CONNECTION_LIMIT_REACHED_SAFE");
				this._invoke_error(null, __get_pw(socket.getOutputStream(), __sock_SO_SNDBUF(socket)), socket, ErrorType.ERROR_BUSY);
				return;
			}
		} else {
			__TIMING("actualhandle::conn_check:", start);
			final InputStream _is = socket.getInputStream();
			final SectionedInputStream si = new SectionedInputStream(_is, REGULAR_REQUEST_DETECTION_SIZE);
			final byte[] initData = new byte[REGULAR_REQUEST_DETECTION_SIZE];
			if (si.read(initData) != 4) {
				log.error("[initialHandle] REGULAR_REQUEST_DETECTION failed! Bailing out...");
				IOUtils.closeAllQuietly(_is, si, socket);
				return;
			}
			IOUtils.closeQuietly(si);
			final InputStream rawInputStream = new SequenceInputStream(si, _is);
			__TIMING("actualhandle::is->initdata:", start);
			final String id = new String(initData, CharsetUtil.US_ASCII);
			final boolean requiresHTTPS = !isRegularRequest(id);
			SSLSocket ssl = null;
			InputStream inputStream;
			if (requiresHTTPS && this.fact != null) {
				ssl = (SSLSocket) this.fact.createSocket(socket, rawInputStream, true);
				ssl.setSSLParameters(this.default_params);
				inputStream = ssl.getInputStream();
				expectHTTPS = true;
				__TIMING("actualhandle::__fact_generate_ssl:", start);
			} else if (requiresHTTPS && this.fact == null) {
				log.warn("Socket [" + socket + "] requested(?) HTTPS...we don't have HTTPS, bailing.");
				IOUtils.closeAllQuietly(rawInputStream, socket);
				return;
			} else {
				inputStream = rawInputStream;
			}
			OutputStream os = ssl != null ? ssl.getOutputStream() : socket.getOutputStream();
			final int prefOutSize = __sock_SO_SNDBUF(socket);
			if (this.settings.bandwidthThrottler != null)
				os = new BandwidthThrottledOutputStream(os, this.settings.bandwidthThrottler);
			final HTTPPrintWriter dos = __get_pw(os, false, prefOutSize);
			__TIMING("actualhandle::__get_pw:", start);
			this.handle((ssl != null ? ssl : socket), inputStream, os, dos, expectHTTPS, start);
		}
	}

	private static final int ENFORCE_DATA_LIMIT_SIZE = 20 * 1024 * 1024;

	public static final int getDataLimitSize() {
		return ENFORCE_DATA_LIMIT_SIZE;
	}

	private static final Splitter TIME_APPLY_TO_SP = Splitter.onPattern("\\, |,");

	@SuppressWarnings("resource")
	public ReadRequest readRequest(final Socket s, final InputStream is) throws Exception {
		final Map<String, String> headers = FastMaps.map(15);
		String header;
		while ((header = readTillNextLine(is)) != null) {
			final String[] sp = Splitters.splitString(header, ":");
			if (sp.length == 0)
				break;
			headers.put(sp[0], ArrayUtils.StringArrayUtils.addFrom(sp, 1, ':').substring(1));
		}
		if (headers.containsKey("range"))
			headers.put("Range", headers.remove("range"));
		Duration dur = null;
		if (headers.containsKey("X-Request-Time-Limit") || headers.containsKey("x-request-time-limit")) {
			try {
				final String tl = headers.containsKey("x-request-time-limit") ? headers.get("x-request-time-limit") : headers.get("X-Request-Time-Limit");
				if (StringsUtil.isNumber(tl)) {
					dur = Duration.ofSeconds((int) StringsUtil.asNumber(tl));
				} else {
					dur = Duration.parse(tl);
				}
			} catch (Exception e) {
				log.error("[readRequest:timeout] Failed to parse duration.", e);
			}
		}
		String[] to = ReadRequest.DEFAULT_APPLY;
		if (headers.containsKey("X-Request-Time-Apply-To") || headers.containsKey("x-request-time-apply-to")) {
			try {
				final String tl = headers.containsKey("x-request-time-apply-to") ? headers.get("x-request-time-apply-to")
						: headers.get("X-Request-Time-Apply-To");
				to = TIME_APPLY_TO_SP.splitToArray(tl.toLowerCase()); // tl.toLowerCase().split("\\, |,");
			} catch (Exception e) {
				log.error("[readRequest:timeout_apply_to] Failed to parse.", e);
			}
		}
		if (__has_header(headers, "Content-Length")) {
			final int cl = MathUtil.safeParseInt(headers.get("Content-Length"), -1);
			log.trace("readRequest() -> Reading " + cl + " bytes of data.");
			final String contentType = headers.get("Content-Type");
			if (this.settings.enforceDataSizeLimit && cl > ENFORCE_DATA_LIMIT_SIZE) {
				log.error("readRequest() -> Data uploaded is too large! (enforceDataSizeLimit skips data larger than 20MBs.)");
				return new ReadRequest(headers).setTimeoutDuration(dur).setTimeoutApplyTo(to);
			} else if (cl > 0) {
				if (contentType != null && contentType.equalsIgnoreCase("multipart/form-data")) {
					log.error("readRequest() -> We aren't implemented yet, skipping.");
				} else { // Unknown/application/x-www-form-urlencoded
					return new ReadRequest(headers, new BoundedInputStream(is, cl, false)).setTimeoutDuration(dur).setTimeoutApplyTo(to);
				}
			} else {
				// No content-length or invalid length...
				return new ReadRequest(headers).setTimeoutDuration(dur).setTimeoutApplyTo(to);
			}
		} else if (headers.containsKey("Transfer-Encoding")) {
			/// XXX: Implement support for chunked POST.
		}
		return new ReadRequest(headers).setTimeoutDuration(dur).setTimeoutApplyTo(to);
	}

	public enum ErrorType { // FIXME: We should be protected once NX is merged
		ERROR_400,
		ERROR_404,
		ERROR_500,
		ERROR_503,
		ERROR_BUSY,
		ERROR_BAD_RESPONSE,
		ERROR_SECURITY_CHECK
	}

	private void _invoke_error(final InputStream is, final HTTPPrintWriter out, final Socket s, final ErrorType et) throws Exception {
		String data;
		if (this.ERROR_PAGES.isEmpty()) {
			data = HARDCODED_ERROR;
			out.println("HTTP/1.1 503 Internal Error Occured");
		} else {
			switch (et) {
				case ERROR_400: {
					data = this.ERROR_PAGES.get("INVALID_REQUEST");
					out.println("HTTP/1.1 400 Bad Request");
					break;
				}
				case ERROR_404:
					data = this.ERROR_PAGES.get("PAGE_NOT_FOUND");
					out.println("HTTP/1.1 404 Page Not Found");
					break;
				case ERROR_500:
					data = this.ERROR_PAGES.get("INTERNAL_ERROR");
					out.println("HTTP/1.1 500 Internal Error Occured");
					break;
				case ERROR_503:
					data = this.ERROR_PAGES.get("SERVICE_UNAVAILABLE");
					out.println("HTTP/1.1 503 Service Unavailable");
					break;
				case ERROR_BAD_RESPONSE:
					data = this.ERROR_PAGES.get("BAD_RESPONSE");
					out.println("HTTP/1.1 500 Internal Error Occured");
					break;
				case ERROR_BUSY:
					data = this.ERROR_PAGES.get("SERVER_BUSY");
					out.println("HTTP/1.1 509 Bandwidth Limit Exceeded");
					break;
				case ERROR_SECURITY_CHECK:
					data = this.ERROR_PAGES.get("SECURITY_CHECK_FAIL");
					out.println("HTTP/1.1 400 Bad Request");
					break;
				default:
					throw new NullPointerException("Unknown error type: " + et);
			}
		}
		if (StringsUtil.isBlank(data))
			data = HARDCODED_ICE_ERROR;
// TODO: Replace below with the StringsUtil version.
//		StringsUtil.replace(data, "$HTTPServer.WebServerName", this.settings.getServerName());
		data = data.replaceAll(Pattern.quote("$HTTPServer.WebServerName"), this.settings.getServerName());
		out.println("Content-type: text/html");
		out.println("Server-name: " + this.settings.getServerName());
		out.println("Server: HttpServer3 from nullUtil 3.0");
		out.println("Content-Length: " + data.getBytes().length);
		out.println("Connection: close");
		out.println("");
		out.print(data);
		out.flush();
		IOUtils.closeAllQuietly(is, out, s);
	}

	private boolean isValid(final Response resp) {
		if (resp == null) {
			log.debug("[isValid] ERROR_REASON: RESPONSE_WAS_NULL");
			return false;
		}
		if (resp.getResponseCode() <= 100 || resp.getResponseCode() >= 600) {
			log.debug("[isValid] ERROR_REASON: INVALID_RESPONSE_CODE [code: " + resp.getResponseCode() + "]");
			return false;
		}
/// XXX: Shouldn't we be checking for a lack of NIO-copyable?
		if (resp.getInputStream() == null && !resp.copyable()) {
			log.debug("[isValid] ERROR_REASON: NO_RESPONSE_STREAM");
			return false;
		}
		return true;
	}

	private static final int READ_LIMIT = 65536; // 16384;
	private static final int CARRIAGE_RETURN = '\r';
	private static final int NEW_LINE = '\n';

	private String readTillNextLine(final InputStream is) throws Exception {
		final ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(IOUtils.STRING_ALLOC_BUFFER);
		int c;
		while ((c = is.read()) != -1) {
			if (c == CARRIAGE_RETURN || c == NEW_LINE) {
				if (c == CARRIAGE_RETURN)
					is.read();
				break;
			}
			byteArrayOutputStream.write(c);
			if (byteArrayOutputStream.size() + 1 > READ_LIMIT) {
				log.warn("Hit URL read limit, bailing out of readTillNextLine!");
				break;
			}
		}
		if (c == -1 && byteArrayOutputStream.size() == 0) {
			return null;
		}
		final String line = byteArrayOutputStream.toString("UTF-8");
		IOUtils.closeQuietly(byteArrayOutputStream);
		return line;
	}

	private String __get_mime(final Page page) throws IOException {
		if (page instanceof HttpPage2) {
			return ((HttpPage2) page).getMimeType();
		} else if (page instanceof HttpPage) {
			if (((HttpPage) page).isCustomMime()) {
				return ((HttpPage) page).getMime();
			}
		} else if (page instanceof CachedPage) {
			final Page p = ((CachedPage) page).page();
			if (p instanceof HttpPage2) {
				return ((HttpPage2) p).getMimeType();
			} else if (p instanceof HttpPage) {
				if (((HttpPage) p).isCustomMime()) {
					return ((HttpPage) p).getMime();
				}
			}
		}
		return null;
	}

	private final CopyAcceleration ccm = CopyAcceleration.get();

	private boolean __is_valid_request(final ReadRequest r, final RequestProtocolType type, final String method) {
		if (!r.header("Host") && type != RequestProtocolType.STTP_0_9) {
			/* HTTP requires a Host header. */
			return false;
		}
		if (!RequestType.isValidMethod(method)) {
			/* HttpServer doesn't understand non-standard methods. */
			return false;
		}
		return true;
	}

	private boolean handleHTTP(final String request0, final Socket socket, final InputStream is, final OutputStream os, final HTTPPrintWriter out,
			final long start, final boolean expectHTTPS, final variable<Boolean> __keep_alive) throws Exception {
		final String[] request = Splitters.splitString(request0, " ");
		if (request.length <= 1) {
			this._invoke_error(is, out, socket, ErrorType.ERROR_400);
			return false;
		}
		final String REQUEST_TYPE = request[0];
		String REQUEST_URI = "/";
		try {
			REQUEST_URI = QUERY_SPLIT.splitToArray(request[1])[0];
		} catch (Exception e) {
			log.error("Failed to get REQUEST_URI!", e);
		}
		String requestVersion = "HTTP/1.0";
		try {
			requestVersion = request[2];
		} catch (Exception e) {
			log.error("Failed to get REQUEST_VERSION! [r0: " + request0 + "]", e);
		}
		RequestProtocolType protocol = RequestProtocolType.guessProtocolFromVersion(requestVersion, expectHTTPS || socket instanceof SSLSocket);
		if (this.isTimingDebugEnabled) {
			log.debug("handle::request_identify: " + ((System.currentTimeMillis() - start) + " ms"));
		}
		if (this.settings.enableProxyBotStop && ProxyBotStop.isProxyBot(REQUEST_URI)) {
			switch (this.settings.proxyBotStopMode) {
				case DROP_CONNECTION: {
					log.warn("[ProxyBotStop] Dropped connection: " + socket);
					__keep_alive.set(false);
					return true;
				}
				case REDIRECT_TO_CF: {
					out.println("HTTP/1.1 301 Moved Permanently");
					out.println("Server: HttpServer3 from nullUtil 3.0");
					out.println("Location: https://www.cloudflare.com/");
					out.println("Connection: close");
					out.println();
					out.println(
							"<html><head>\n" + "<meta http-equiv=\"Refresh\" content=\"0; URL=https://www.cloudflare.com/\" />\n" + "</head>\n" + "</html>");
					out.flush();
					__keep_alive.set(false);
					return true;
				}
				default:
					break;
			}
		}
		if (this.settings.enableBotAway && BotAway.isBot(REQUEST_URI)) {
			log.warn("[BotAway] Dropped connection: " + socket);
			__keep_alive.set(false);
			return true;
		}
		final ReadRequest rr = readRequest(socket, is);
		if (this.isTimingDebugEnabled) {
			log.debug("handle::request_read: " + ((System.currentTimeMillis() - start) + " ms"));
		}
		if (!__is_valid_request(rr, protocol, REQUEST_TYPE)) {
			this._invoke_error(is, out, socket, ErrorType.ERROR_400);
			return false;
		}
		if (this.settings.debugPrintRequests) {
			log.info("\t" + REQUEST_TYPE + " [" + (rr.hashCode()) + "]");
			log.info("\t" + request[1]);
			log.info("\t" + requestVersion);
			for (final Map.Entry<String, String> header : rr.headers().entrySet()) {
				log.info("\t\t" + header.getKey() + ": " + header.getValue(), true);
			}
		}
		// WebSocket Entrypoint
		if (rr.header("Upgrade") && rr.headerIs("Upgrade", "websocket") && this.flags.enable_ws) {
			final Page p = this.getPage(REQUEST_URI, socket);
			if (p == null) {
				this._invoke_error(is, out, socket, ErrorType.ERROR_404);
				return false;
			} else if (!(p instanceof IWebSocketPage)) {
				this._invoke_error(is, out, socket, ErrorType.ERROR_400);
				return false;
			} else {
				final IWebSocketPage iws = (IWebSocketPage) p;
				log.debug("handle::WS_UPGRADE: " + rr.header("Upgrade"));
				if (iws.prefersLowLatency()) {
					try {
						socket.setTcpNoDelay(true);
					} catch (Exception ex) {
					}
				}
				final String key = rr.headers().get("Sec-WebSocket-Key");
				out.println("HTTP/1.1 101 Switching Protocols");
				out.println("Connection: Upgrade");
				out.println("Upgrade: websocket");
				out.println("Sec-WebSocket-Accept: " + ProtocolUtils.getMagicPayload(key));
				if (iws.hasProtocolName()) {
					out.print("Sec-WebSocket-Protocol: ");
					out.println(iws.getProtocolName());
				}
				out.println("Server: HttpServer3 from nullUtil 3.0");
				out.println();
				out.flush();
				final Peer peer = new Peer(socket, is, os, rr.headers()) {
					@Override
					public void onConnected() {
						iws.onPeerConnect(this);
					}

					@Override
					public void onBinaryMessage(byte[] message) {
						iws.onBinaryMessage(this, message);
					}

					@Override
					public void onTextMessage(String message) {
						iws.onTextMessage(this, message);
					}

					@Override
					public void onClose(final CloseReason reason) {
						iws.onPeerClose(this, reason);
					}
				};
				peer.run();
				return false;
			}
		} else if (rr.header("Accept") && rr.headerIs("Accept", "text/event-stream") && this.flags.enable_sse) {
			final Page p = this.getPage(REQUEST_URI, socket);
			if (p == null || !(p instanceof SSEPage)) {
				this._invoke_error(is, out, socket, ErrorType.ERROR_404);
				return false;
			}
			final SSEPage s = (SSEPage) p;
			if (s.prefersLowLatency()) {
				try {
					socket.setTcpNoDelay(true);
				} catch (Exception ex) {
				}
			}
			log.debug("handle::SSE_UPGRADE: " + rr.header("Accept"));
			out.println("HTTP/1.1 200 OK");
			out.println("Server: HttpServer3 from nullUtil 3.0");
			out.println("Connection: keep-alive");
			out.println("Content-Type: text/event-stream; charset=utf-8");
			out.println();
			out.flush();
			final SSESession sse = new SSESession(socket, is, os, out, rr, REQUEST_URI) {

				@Override
				public void onConnected(final ReadRequest rr, final String url) {
					s.onConnected(this, rr, url);
				}

			};
			try {
				sse.run();
				IOUtils.closeQuietly(sse);
			} catch (Exception e) {
				log.error("[SSE::\"" + s + "\"] Failed to run session!", e);
				s.onDisconnect(sse);
				IOUtils.closeQuietly(sse);
			}
			// SSE Entrypoint
			return false;
		} else if ((rr.header("Upgrade") || (rr.header("HTTP2-Settings"))) && rr.headerIs("Upgrade", "h2c") && this.flags.enable_http2) {
			// HTTP 2 Entrypoint
			log.debug("handle::HTTP2_UPGRADE: " + rr.header("Upgrade"));
			out.println("HTTP/1.1 101 Switching Protocols");
			out.println("Connection: Upgrade");
			out.println("Upgrade: h2c");
			out.println("Server: HttpServer3 from nullUtil 3.0");
			out.println();
			out.flush();
			os.flush();
			protocol = (expectHTTPS ? RequestProtocolType.HTTPS_2_0 : RequestProtocolType.HTTP_2_0);
			is.read();
			final String _h2_header = readTillNextLine(is);
			if (!Variables.requireNonNullObject(_h2_header, "_h2_header").equals("PRI * HTTP/2.0")
					&& !Variables.requireNonNullObject(_h2_header, "_h2_header").equals("RI * HTTP/2.0")) {
				log.warn("Invalid HTTP/2 upgrade! [got: " + _h2_header + "]");
				return false;
			}
			is.skip(8); // Skip the \r\nSM\r\n\r\n
			final IOContainer container = new IOContainer(socket, is, os);
			ServerThreadUtils.http2Initialization(container, this.hrv, this.rqd);
			IOUtils.closeAllQuietly(is, os, socket);
			return false;
		} else {
			log.debug("request0: " + request0);
			return this.postInit(request0, socket, is, os, out, start, __keep_alive, rr, requestVersion, REQUEST_URI, REQUEST_TYPE, request, protocol,
					expectHTTPS);
		}
	}

	/* H2 */

	final HttpRequestDispatcher rqd = new HttpRequestDispatcher(this);
	final HttpRequestValidator hrv = new HttpRequestValidator(new RequestValidatorConfig());

	/* */

	private boolean __do_response_headers(final Response response, final HTTPPrintWriter out, final long start, final variable<Boolean> __keep_alive,
			final boolean ignoreCompressionWorkaround, final HttpCompressionMethod comp, final Page page, final boolean expectHTTPS,
			final Map<String, String> headers) throws IOException {
		if (this.flags.enable_early_hints && response.hasPreloads()) {
			log.debug("handle::pre_early_hints: " + ((System.currentTimeMillis() - start) + " ms"));
			out.println("HTTP/1.1 103 Early Hints");
			for (final Map.Entry<String, String> preload : response.preloads().entrySet()) {
				out.println("Link: " + preload.getKey() + "; rel=preload; as=" + preload.getValue());
			}
			out.println();
			if (!this.flags.reduce_response_flushing)
				out.flush();
			log.debug("handle::early_hints: " + ((System.currentTimeMillis() - start) + " ms"));
		}
		final int resp_code = response.getA();
		out.println("HTTP/1.1 " + resp_code + " " + stringForResponse(resp_code));
		out.println("Server-name: " + this.settings.getServerName());
		out.println("Server: HttpServer3 from nullUtil 3.0");
		if (this.flags.opt_out_floc)
			out.println("Permissions-Policy: interest-cohort=()");
		if (this.flags.requests_enable_byte_ranges) {
			out.println("Accept-Ranges: bytes");
		}
		if (response.getCustomHeaders() != null && !response.getCustomHeaders().isEmpty()) {
			if (this.settings.debugPrintResponses) {
				log.info("\tRESPONSE");
				for (final Map.Entry<String, String> header : response.getCustomHeaders().entrySet()) {
					log.info("\t\t" + header.getKey() + ": " + header.getValue(), true);
				}
			}
			for (final Map.Entry<String, String> e : response.getCustomHeaders().entrySet()) {
				out.println(e.getKey() + ": " + e.getValue());
			}
		}
		__apply_keep_alive_workarounds(response, ignoreCompressionWorkaround, comp, __keep_alive);
		String mime = __get_mime(page);
		if (response.hasCustomMime()) {
			out.println("Content-Type: " + response.getCustomMime());
		} else if (!response.copyable() && !response.nioCopyable() && response.getInputStream().markSupported()) {
			if (mime == null) {
				try {
					mime = URLConnection.guessContentTypeFromStream(response.getB());
				} catch (Exception e) {
				}
			}
			if (mime != null) {
				out.println("Content-Type: " + mime);
			}
		}
		__apply_caching_headers(page, out);
		if (this.settings.allowCORSFromExternals) {
			out.println("Access-Control-Allow-Origin: *");
			out.println("Access-Control-Allow-Credentials: true");
			out.println("Access-Control-Allow-Headers: *");
		}
		if (this.settings.enableHSTS) {
			if (this.settings.enableHSTSForSubdomains) {
				out.println("Strict-Transport-Security: max-age=31536000; includeSubDomains");
			} else {
				out.println("Strict-Transport-Security: max-age=31536000");
			}
		}
		final boolean promptForHTTPS = this.httpsServerSocket != null && !expectHTTPS && this.httpsPort == 443;
		if (this.settings.enableHTTPSUpgrade && this.settings.httpsUpgradePolicy == HTTPSUpgradePolicy.VIA_CSP && promptForHTTPS) {
			out.println("Content-Security-Policy: upgrade-insecure-requests");
		}
		if (this.flags.enable_http2) {
			out.println("Alt-Svc: h2=\":" + (this.httpsServerSocket != null ? this.httpsPort : this.port) + "\"; ma=2592000;");
		}
		if (this.settings.hasCORS() && !response.hasHeader("Cross-Origin-Resource-Policy")) {
			out.println("Cross-Origin-Resource-Policy: " + this.settings.corsPolicy.value());
		}
		if (this.settings.hasCOEP() && !response.hasHeader("Cross-Origin-Embedder-Policy")) {
			out.println("Cross-Origin-Embedder-Policy: " + this.settings.coepPolicy.value());
		}
		if (this.settings.hasCOOP() && !response.hasHeader("Cross-Origin-Opener-Policy")) {
			out.println("Cross-Origin-Opener-Policy: " + this.settings.coopPolicy.value());
		}
		boolean __enable_chunk_read = headers.containsKey("Range") && this.flags.requests_enable_byte_ranges;
		if (page instanceof HttpPage2) {
			if (__enable_chunk_read && !((HttpPage2) page).allowRanges()) {
				__enable_chunk_read = false;
			}
		}
		if (this.flags.response_guess_content_length) {
			__guess_content_length(comp, response, out, __enable_chunk_read);
		} else if (hasBlankResponse(response)) {
			out.println("Content-Length: 0");
		} else {
			if (!response.__has_header("Content-Length") && !this.flags.enable_chunked_transfers && !this.flags.enable_compressed_chunked_transfers) {
				/* Disable Keep-Alive on responses where we don't know the length. */
				__keep_alive.set(false);
			}
		}

		return __enable_chunk_read;
	}

	private void __apply_caching_headers(final Page page, final HTTPPrintWriter out) {
		if (page instanceof HttpPage3 && ((HttpPage3) page).isCaching()) {
			if (!((HttpPage3) page).getCachingMethod().equals(CachingMethod.BLANK)) {
				out.println("Cache-Control: " + ((HttpPage3) page).getCachingMethod().getStringValue());
				if (((HttpPage3) page).getCachingMethod().equals(CachingMethod.NO_CACHE) && this.settings.enableRFC9111Compat) {
					out.println("Pragma: no-cache");
				}
			}
		} else if (page instanceof CachedPage && ((CachedPage) page).page() instanceof HttpPage3) {
			if (((HttpPage3) ((CachedPage) page).page()).isCaching()) {
				if (!((HttpPage3) ((CachedPage) page).page()).getCachingMethod().equals(CachingMethod.BLANK)) {
					out.println("Cache-Control: " + ((HttpPage3) ((CachedPage) page).page()).getCachingMethod().getStringValue());
					if (((HttpPage3) ((CachedPage) page).page()).getCachingMethod().equals(CachingMethod.NO_CACHE) && this.settings.enableRFC9111Compat) {
						out.println("Pragma: no-cache");
					}
				}
			}
		}
	}

	private static boolean isFixedInputStream(final InputStream is) {
		return is instanceof ByteArrayInputStream;
	}

	private static boolean __qualifies_for_compression(final Response resp, final Page page) {
		if (resp.getInputStream() != null && isFixedInputStream(resp.getInputStream()))
			return false;
		return !resp.shouldDeferCompression();
	}

	private void handleRespondedPage(final Response response, final Page page, final variable<Boolean> __keep_alive, final InputStream is,
			final OutputStream os, final HTTPPrintWriter out, final Socket socket, final Map<String, String> headers, final HttpPage3.Request ar,
			final long start, final RequestProtocolType protocol, final boolean expectHTTPS) throws Exception {
		try {
			if (this.isTimingDebugEnabled) {
				log.debug("handle::handleRespondedPage: " + ((System.currentTimeMillis() - start) + " ms"));
			}
			if (!this.isValid(response)) {
				log.debug("[invoke_error] Cause: INVALID_RESPONSE");
				this._invoke_error(is, out, socket, ErrorType.ERROR_BAD_RESPONSE);
				__keep_alive.set(false);
				return;
			}
			if (this.isTimingDebugEnabled) {
				log.debug("handle::post_valid_check: " + ((System.currentTimeMillis() - start) + " ms"));
			}
			if (response.shouldDisableKeepAlive())
				__keep_alive.set(false);

			HttpCompressionMethod comp = HttpCompressionMethod.NONE;
			if (__qualifies_for_compression(response, page)) {
				comp = HttpCompressionMethod.parse(headers.get("Accept-Encoding"));
			}

			if (comp.equals(HttpCompressionMethod.GZIP) && !this.flags.responses_enable_gzip_encoding) {
				comp = HttpCompressionMethod.NONE;
				__keep_alive.set(false);
			}

			if (this.isTimingDebugEnabled) {
				log.debug("handle::core_request_finished: " + ((System.currentTimeMillis() - start) + " ms"));
			}

			final boolean ignoreCompressionWorkaround = this.flags.enable_compressed_chunked_transfers && this.flags.enable_chunked_transfers;
			final boolean __enable_chunk_read = __do_response_headers(response, out, start, __keep_alive, ignoreCompressionWorkaround, comp, page, expectHTTPS,
					headers);

			if (this.isTimingDebugEnabled) {
				log.debug("handle::response_ready: " + ((System.currentTimeMillis() - start) + " ms"));
			}

			HttpServer3.this.__write_to_socket(socket, comp, headers, out, response, is, os, page,
					response.shouldDisableKeepAlive() ? LOCKED_FALSE : __keep_alive, ccm, start, __enable_chunk_read, protocol);
			if (this.isTimingDebugEnabled) {
				log.debug("handle::socket_write_complete: " + ((System.currentTimeMillis() - start) + " ms"));
			}
			IOUtils.closeAllQuietly(response, ar);
		} catch (Exception e) {
			IOUtils.closeAllQuietly(is, response, ar, socket, os, out);
			__keep_alive.set(false);
			if (isIgnorableException(e)) {
				log.derror("handleRespondedPage() -> Request write failed, cancelling response. [error: " + e + "]");
			} else
				throw new RuntimeException(e);
		}
	}

	private static final variable<Boolean> LOCKED_FALSE = variable.makeLocked(false);

	private boolean postInit(final String request0, final Socket socket, final InputStream is, final OutputStream os, final HTTPPrintWriter out,
			final long start, final variable<Boolean> __keep_alive, final ReadRequest rr, final String requestVersion, final String REQUEST_URI,
			final String REQUEST_TYPE, final String[] request, final RequestProtocolType protocol, final boolean expectHTTPS) throws Exception {
		final boolean promptHTTPSUpgrade = !expectHTTPS && this.settings.enableHTTPSUpgrade
				&& this.settings.httpsUpgradePolicy == HTTPSUpgradePolicy.VIA_UPGRADE_INSECURE_REQUESTS && this.httpsPort == 443;
		if (promptHTTPSUpgrade && rr.headerIs("Upgrade-Insecure-Requests", "1") && (true == false)) {
			// FIXME: We need to provide a index domain so we can do U-I-R...
			out.println("HTTP/1.1 307 Moved Temporarily");
			out.println("Location: https://example.com/");
			out.println("Vary: Upgrade-Insecure-Requests");
			out.println("Server-name: " + this.settings.getServerName());
			out.println("Server: HttpServer3 from nullUtil 3.0");
			out.println("Cache-Control: no-store");
			out.println();
			__keep_alive.set(false);
			return true;
		}
		final Map<String, String> headers = rr.headers();
		if (this.settings.enableCrawlerBlock && CrawlerBlock.isCrawler(headers.getOrDefault("User-Agent", "null"))) {
			__keep_alive.set(false);
			return true;
		}
		__check_and_set_keepalive(protocol, __keep_alive, headers);
		if (this.isTimingDebugEnabled) {
			log.debug("handle::full_request: " + ((System.currentTimeMillis() - start) + " ms"));
		}
		Page _page = getPage(REQUEST_URI, socket);
		final boolean exclude_from_captcha = (_page instanceof HttpPage3) && (((HttpPage3) _page).isExcludedFromCaptcha());
		if (this.isTimingDebugEnabled) {
			log.debug("handle::getPage: " + ((System.currentTimeMillis() - start) + " ms"));
		}

		if (this.settings.enableNCaptcha) {
			final boolean valid_captcha = UUIDCookieLoader.validateFromHeaders(headers);
			if (!exclude_from_captcha && !valid_captcha && !REQUEST_URI.startsWith("/captcha/")
					&& this.settings.nCaptchaMethod == nCaptchaEnforcementMethod.PRE_CONNECT) {
				if (_page instanceof HttpPage3 && ((HttpPage3) _page).shouldUseJSONCaptchaPage()) {
					_page = this.getPage("/captcha-json", socket);
				} else {
					_page = this.getPage("/captcha", socket);
				}
			}
		}

		final Page page = _page;
		if (page == null) {
			log.debug("[invoke_error] Cause: PAGE_NOT_FOUND");
			this._invoke_error(is, out, socket, ErrorType.ERROR_404);
			return true;
		}
		if (page instanceof IWebSocketPage) {
			log.debug("[invoke_error] Cause: INVALID_REQUEST (WEBSOCKET)");
			this._invoke_error(is, out, socket, ErrorType.ERROR_400);
			return true;
		}
		if (this.isTimingDebugEnabled) {
			log.debug("handle::page_ready: " + ((System.currentTimeMillis() - start) + " ms"));
		}
		final RequestType type = RequestType.fetch(REQUEST_TYPE);
		if (this.isTimingDebugEnabled) {
			log.debug("handle::page_pre_validate: " + ((System.currentTimeMillis() - start) + " ms"));
		}
		if (page instanceof HttpPage3) {
			if (HttpPageRequestValidator.checkSecurity(type, rr, ((HttpPage3) page))) {
				log.debug("[invoke_error] Cause: SECURITY_CHECK_FAILED");
				this._invoke_error(is, out, socket, ErrorType.ERROR_SECURITY_CHECK);
				__keep_alive.set(false);
				return false;
			}
		}
		if (this.isTimingDebugEnabled) {
			log.debug("handle::page_pre_exec: " + ((System.currentTimeMillis() - start) + " ms"));
		}
		try {
			// Response Code, ResponseStream, Response Headers
			if (page instanceof ASyncHttpPage3) { // use new logic.
				final ControllableFutureTask<Response> r = new ControllableFutureTask<Response>();
				final HttpPage3.Request ar = new HttpPage3.Request(request[1], rr.hasStream() ? rr.stream() : HttpPage3.BLANK_IO_STREAM,
						socket.getInetAddress().getHostAddress(), protocol, socket);
				if (this.isTimingDebugEnabled) {
					log.debug("handle::async_request_make: " + ((System.currentTimeMillis() - start) + " ms"));
				}
				if (this.flags.enable_queued_requests) {
					td.asyncSubmit(() -> {
						((ASyncHttpPage3) page).onFutureRequest(ar.importHeaders(headers).setRequestType(type), r);
					});
				} else {
					((ASyncHttpPage3) page).onFutureRequest(ar.importHeaders(headers).setRequestType(type), r);
				}
				if (this.isTimingDebugEnabled) {
					log.debug("handle::async_request_call: " + ((System.currentTimeMillis() - start) + " ms"));
				}
				r.addCallback(new AsyncTaskCallback<Response>() {

					@Override
					public void onCompletion(Response result) {
						if (HttpServer3.this.isTimingDebugEnabled) {
							log.debug("handle::async_response: " + ((System.currentTimeMillis() - start) + " ms"));
						}
						try {
							HttpServer3.this.handleRespondedPage(result, page, __keep_alive, is, os, out, socket, headers, ar, start, protocol, expectHTTPS);
						} catch (Exception e) {
							__keep_alive.set(false);
							IOUtils.closeAllQuietly(is, os, out, socket, ar);
							if (e instanceof SocketTimeoutException)
								log.warn("Encountered a socket timeout, closing connection.");
							else if (e instanceof SocketException || e.getMessage().contains("Broken pipe (Write failed)"))
								log.warn("Encountered a socket error, closing connection.");
							else
								throw new RuntimeException(e);
						}
					}

					@Override
					public void onError(Throwable e) {
						throw new RuntimeException(e);
					}
				});
				if (rr.hasTimeoutDuration() && rr.appliesTimeoutTo("request")) {
					r.waitFor(rr.getTimeoutDuration().toMillis(), TimeUnit.MILLISECONDS);
				} else {
					r.waitTillCompletion();
				}
				if (!r.completed()) {
					r.kill();
					__keep_alive.set(false);
					IOUtils.closeAllQuietly(is, os, out, socket);
					return false;
				} else {
					return !__keep_alive.get();
				}
			} else {
				if (this.flags.enable_queued_requests) {
					final Executable<?> x = td.asyncSubmit(() -> {
						try {
							if (this.isTimingDebugEnabled) {
								log.debug("handle::queued_entry: " + ((System.currentTimeMillis() - start) + " ms"));
							}
							try {
								final Response response = this.do_response(type, request, rr, socket, headers, page, protocol);
								if (this.isTimingDebugEnabled) {
									log.debug("handle::do_response_queued: " + ((System.currentTimeMillis() - start) + " ms"));
								}
								this.handleRespondedPage(response, page, __keep_alive, is, os, out, socket, headers, null, start, protocol, expectHTTPS);
								return null;
							} catch (Exception e) {
								__keep_alive.set(false);
								IOUtils.closeAllQuietly(is, os, out, socket);
								if (e instanceof SocketTimeoutException) {
									log.warn("Encountered a socket timeout, closing connection.");
									return null;
								} else
									throw e;
							}
						} catch (Throwable e) {
							throw new RuntimeException(e);
						}
					});
					if (rr.hasTimeoutDuration() && rr.appliesTimeoutTo("request")) {
						x.waitFor(rr.getTimeoutDuration().toMillis(), TimeUnit.MILLISECONDS);
					} else {
						x.waitTillCompletion();
					}
					if (!x.completed())
						x.kill();
					return !__keep_alive.get();
				} else {
					if (this.isTimingDebugEnabled) {
						log.debug("handle::entry: " + ((System.currentTimeMillis() - start) + " ms"));
					}
					try {
						final Response response = this.do_response(type, request, rr, socket, headers, page, protocol);
						if (this.isTimingDebugEnabled) {
							log.debug("handle::do_response: " + ((System.currentTimeMillis() - start) + " ms"));
						}
						this.handleRespondedPage(response, page, __keep_alive, is, os, out, socket, headers, null, start, protocol, expectHTTPS);
						return !__keep_alive.get();
					} catch (Exception e) {
						__keep_alive.set(false);
						IOUtils.closeAllQuietly(is, os, out, socket);
						log.error("Response failed!", e);
						return false;
					}
				}
			}
		} catch (Exception e) {
			if (isIgnorableException(e)) {
				log.debug("[invoke_error] Cause: ERROR_IS_IGNORABLE");
			} else {
				log.error("handle() -> Failed to process client!", e);
			}
			this._invoke_error(is, out, socket, ErrorType.ERROR_503);
			IOUtils.closeAllQuietly(is, out, os, socket);
			return true;
		}
	}

	private void handle(final Socket socket, final InputStream _is, final OutputStream _os, final HTTPPrintWriter out, final boolean expectHTTPS,
			final long start) throws Exception {
		final variable<Boolean> __keep_alive = new variable<Boolean>(false);
		final InputStream is = IOUtils.nonCloseable(_is);
		final OutputStream os = IOUtils.nonCloseable(_os);
		do {
			if (socket.isClosed()) {
				log.error("handle(): Uhh, the socket is closed, bailing out of this request...");
				IOUtils.closeAllQuietly(socket, is, out);
				__keep_alive.set(false);
				break;
			}
			final String request0;

			try {
				request0 = readTillNextLine(is);
			} catch (SocketTimeoutException _ignored) {
				log.derror("Encountered SocketTimeoutException in readTillNextLine, discarding socket...");
				break;
			}

			if (this.isTimingDebugEnabled) {
				log.debug("handle::readRequest0: " + ((System.currentTimeMillis() - start) + " ms"));
			}
			if (request0 == null || request0.isEmpty()) {
				IOUtils.closeAllQuietly(out, is, socket);
				__keep_alive.set(false);
				break;
			} else if (request0.equals("PRI * HTTP/2.0") && this.flags.enable_http2) {
				log.debug("actualHandle::HTTP2: Creating SESSION");
				is.skip(8);
				final IOContainer container = new IOContainer(socket, is, os);
				ServerThreadUtils.http2Initialization(container, this.hrv, this.rqd);
				IOUtils.closeAllQuietly(is, os, socket);
				break;
			} else if (request0.endsWith("STTP/1.0")) {
				/* Removed in 3.0.6 */
				IOUtils.closeAllQuietly(out, is, socket);
				__keep_alive.set(false);
				break;
			} else {
				if (this.handleHTTP(request0, socket, is, os, out, start, expectHTTPS, __keep_alive)) {
					break;
				}
			}
		} while (__keep_alive.object() && !socket.isClosed() && !socket.isInputShutdown());
		__TIMING("handle::closeAllQuietly", start);
		IOUtils.closeInputQuietly(socket);
		IOUtils.closeOutputQuietly(socket);
	}

	@SuppressWarnings("resource")
	private Response do_response(final RequestType type, final String[] request, final ReadRequest rr, final Socket socket, final Map<String, String> headers,
			final Page page, final RequestProtocolType protocol) throws IOException {
		if (page instanceof HttpPage3) {
			final HttpPage3.Request req = new HttpPage3.Request(request[1], rr.hasStream() ? rr.stream() : HttpPage3.BLANK_IO_STREAM,
					socket.getInetAddress().getHostAddress(), protocol, socket).importHeaders(headers).setRequestType(type);
			return ((HttpPage3) page).onRequest(req);
		} else if (page instanceof HttpPage2) {
			final byte[] extraData = rr.hasStream() ? IOUtils.toByteArray(rr.stream()) : null;
			return new Response(((HttpPage2) page).request(type, request[1], rr.headers_legacy(), extraData, socket.getInetAddress().getHostAddress()));
		} else if (page instanceof HttpPage) {
			final HttpPage p = ((HttpPage) page);
			if (p.isOverBytes()) {
				return new Response(new DataTriplet<Integer, InputStream, Map<String, String>>(200,
						new ByteArrayInputStream(p.handleRequest_Alt(request[1], socket.getInetAddress().getHostAddress(), type)),
						new TreeMap<String, String>()));
			} else if (p.sendRaw()) {
				return new Response(
						new DataTriplet<Integer, InputStream, Map<String, String>>(200, new FileInputStream(p.getPath()), new TreeMap<String, String>()));
			} else {
				return new Response(new DataTriplet<Integer, InputStream, Map<String, String>>(200,
						new ByteArrayInputStream(p.handleRequest(request[1], socket.getInetAddress().getHostAddress(), type).getBytes()),
						new TreeMap<String, String>()));
			}
		} else if (page instanceof CachedPage) {
			final CachedPage cp = (CachedPage) page;
			return new Response(200, cp, cp);
		} else {
			return null;
		}
	}

	private OutputStream get_os(final Response response, final Socket socket, final HTTPPrintWriter out, final OutputStream os, HttpCompressionMethod comp,
			final boolean shouldUseChunked) throws IOException {
		if ((this.flags.enable_chunked_transfers || response.shouldPreferChunking()) && shouldUseChunked) {
			switch (comp) {
				case NONE:
					break;
				case GZIP: {
					out.println("Content-Encoding: gzip");
					break;
				}
				case DEFLATE: {
					out.println("Content-Encoding: deflate");
					break;
				}
				case ZSTD_NO_DICT: {
					out.println("Content-Encoding: zstd");
					break;
				}
				case COMPRESS: {
					out.println("Content-Encoding: compress");
					break;
				}
				case SNAPPY: {
					out.println("Content-Encoding: x-snappy-framed");
					break;
				}
				default:
					break;
			}
			out.println();
			if (!this.flags.reduce_response_flushing)
				out.flush();
			return new ChunkedOutputStream(IOUtils.nonCloseable(os));
		} else if (response.isNativeEncoded.get() && response.nativeEncoding != null && !response.nativeEncoding.isEmpty()) {
			if (!response.nativeEncoding.equalsIgnoreCase("none")) {
				out.println("Content-Encoding: " + response.nativeEncoding);
				out.println();
				if (!this.flags.reduce_response_flushing)
					out.flush();
			} else {
				out.println();
				if (!this.flags.reduce_response_flushing)
					out.flush();
			}
			return os;
		} else if (comp == HttpCompressionMethod.DEFLATE) {
			out.println("Content-Encoding: deflate");
			out.println();
			if (!this.flags.reduce_response_flushing)
				out.flush();
			return new DeflaterOutputStream(os, new Deflater(9, true));
		} else if (comp == HttpCompressionMethod.GZIP && this.flags.responses_enable_gzip_encoding) {
			out.println("Content-Encoding: gzip");
			out.println();
			if (!this.flags.reduce_response_flushing)
				out.flush();
			if (this.flags.use_migz_for_gzip)
				return new MiGzOutputStream(os);
			else if (this.flags.use_parallel_gzip)
				return new ParallelGZIPOutputStream(os);
			else
				return new GZIPOutputStream(os, __sock_SO_SNDBUF(socket), true);
		} else if (comp == HttpCompressionMethod.COMPRESS) {
			out.println("Content-Encoding: compress");
			out.println();
			if (!this.flags.reduce_response_flushing)
				out.flush();
			return new LZCOutputStream(os);
		} else if (comp == HttpCompressionMethod.ZSTD_NO_DICT) {
			out.println("Content-Encoding: zstd");
			out.println();
			if (!this.flags.reduce_response_flushing)
				out.flush();
			return new ZstdOutputStream(os);
		} else if (comp == HttpCompressionMethod.SNAPPY) {
			out.println("Content-Encoding: x-snappy-framed");
			out.println();
			if (!this.flags.reduce_response_flushing)
				out.flush();
			return new SnappyFramedOutputStream(os);
		} else {
			out.println();
			if (!this.flags.reduce_response_flushing)
				out.flush();
			return null;
		}
	}

	private void __do_chunked_transfer(final InputStream r, final OutputStream o, final HttpCompressionMethod method) throws IOException {
		try {
			if (method == null || method == HttpCompressionMethod.NONE) {
				copy(this.ccm, r, o);
				((ChunkedOutputStream) o).writeEOF();
			} else {
				OutputStream ob;
				switch (method) {
					case GZIP: {
						if (this.flags.use_migz_for_gzip)
							ob = new MiGzOutputStream(o);
						else if (this.flags.use_parallel_gzip)
							ob = new ParallelGZIPOutputStream(o);
						else
							ob = new GZIPOutputStream(o);
						break;
					}
					case DEFLATE: {
						ob = new DeflaterOutputStream(o, new Deflater(9, true));
						break;
					}
					case COMPRESS: {
						ob = new LZCOutputStream(o);
						break;
					}
					case ZSTD_NO_DICT: {
						ob = new ZstdOutputStream(o);
						break;
					}
					case SNAPPY: {
						ob = new SnappyFramedOutputStream(o);
						break;
					}
					default:
						throw new UnsupportedOperationException("Cannot chunk: " + method);
				}
				copy(this.ccm, r, ob);
				try {
					__finish_stream(ob);
					ob.flush();
				} catch (Exception n) {
				}
				if (!this.flags.reduce_response_flushing)
					o.flush();
				((ChunkedOutputStream) o).writeEOF();
				IOUtils.closeAllQuietly(ob);
			}
		} catch (Exception e) {
			if (!isIgnorableException(e))
				log.error("__do_chunked_transfer: Connection failed?", e);
		}
	}

	private void __do_chunked_copyable(final Copyable r, final OutputStream o, final HttpCompressionMethod method) throws IOException {
		try {
			if (method == null || method == HttpCompressionMethod.NONE) {
				r.writeTo(o);
				((ChunkedOutputStream) o).writeEOF();
			} else {
				OutputStream ob;
				switch (method) {
					case GZIP: {
						if (this.flags.use_migz_for_gzip) {
							ob = new MiGzOutputStream(o);
						} else if (this.flags.use_parallel_gzip) {
							ob = new ParallelGZIPOutputStream(o);
						} else {
							ob = new GZIPOutputStream(o);
						}
						break;
					}
					case DEFLATE: {
						ob = new DeflaterOutputStream(o, new Deflater(9, true));
						break;
					}
					case COMPRESS: {
						ob = new LZCOutputStream(o);
						break;
					}
					case ZSTD_NO_DICT: {
						ob = new ZstdOutputStream(o);
						break;
					}
					case SNAPPY: {
						ob = new SnappyFramedOutputStream(o);
						break;
					}
					default:
						throw new UnsupportedOperationException("Cannot chunk: " + method);
				}
				r.writeTo(ob);
				try {
					__finish_stream(ob);
					ob.flush();
				} catch (Exception n) {
				}
				((ChunkedOutputStream) o).writeEOF();
				IOUtils.closeAllQuietly(ob);
			}
			if (!this.flags.reduce_response_flushing)
				o.flush();
		} catch (Exception e) {
			if (!isIgnorableException(e))
				log.error("__do_chunked_copyable: Connection failed?", e);
		}
	}

	private void __do_chunked_nio_copyable(final NIOCopyable r, final OutputStream _os, final WritableByteChannel wbc, final HttpCompressionMethod method)
			throws IOException {
		try {
			if (method == null || method == HttpCompressionMethod.NONE) {
				final WritableByteChannel wbc_compat = IOUtils.copyMakeChannel(_os);
				r.writeTo(wbc_compat);
				((ChunkedOutputStream) _os).writeEOF();
			} else {
				OutputStream ob;
				switch (method) {
					case GZIP: {
						if (this.flags.use_migz_for_gzip)
							ob = new MiGzOutputStream(_os);
						else if (this.flags.use_parallel_gzip)
							ob = new ParallelGZIPOutputStream(_os);
						else
							ob = new GZIPOutputStream(_os);
						break;
					}
					case DEFLATE: {
						ob = new DeflaterOutputStream(_os, new Deflater(9, true));
						break;
					}
					case COMPRESS: {
						ob = new LZCOutputStream(_os);
						break;
					}
					case ZSTD_NO_DICT: {
						ob = new ZstdOutputStream(_os);
						break;
					}
					case SNAPPY: {
						ob = new SnappyFramedOutputStream(_os);
						break;
					}
					default:
						throw new UnsupportedOperationException("Cannot chunk: " + method);
				}
				final WritableByteChannel wbc_compat = IOUtils.copyMakeChannel(ob);
				r.writeTo(wbc_compat);
				try {
					__finish_stream(ob);
					ob.flush();
				} catch (Exception n) {
				}
				((ChunkedOutputStream) _os).writeEOF();
				IOUtils.closeAllQuietly(ob, wbc_compat);
			}
		} catch (Exception e) {
			if (!isIgnorableException(e))
				log.error("__do_chunked_nio_copyable: Connection failed?", e);
		}
	}

	private void __do_byte_ranges(final ByteReadRange[] byteRanges, final InputStream r, final OutputStream os) throws IOException {
		int previousMin = -1;
		for (final ByteReadRange brr : byteRanges) {
			final boolean wasPreviousBad = (previousMin == -1);
			byte[] disposableBuffer = new byte[(int) brr.min];
			previousMin = (int) brr.min;
			if (brr.min <= 0) {
				// no read required;
			} else {
				if (!wasPreviousBad) {
					disposableBuffer = new byte[(int) (brr.min - previousMin)];
					r.read(disposableBuffer);
				} else {
					r.read(disposableBuffer);
				}
			}
			disposableBuffer = new byte[(int) brr.toRead()];
			r.read(disposableBuffer);
			os.write(disposableBuffer);
		}
	}

	private void __nio_copyable(final boolean shouldUseChunked, final Response response, final OutputStream os, final OutputStream _os,
			final HttpCompressionMethod comp, final Socket socket, final variable<Boolean> __keep_alive, final HTTPPrintWriter out, final InputStream is,
			final long start) throws Exception {
		try {
			if (shouldUseChunked) {
				this.__do_chunked_nio_copyable(response.getNIOCopyable(), (os != null ? os : _os), IOUtils.copyMakeChannel(os != null ? os : _os), comp);
			} else {
				response.getNIOCopyable().writeTo(IOUtils.copyMakeChannel(os != null ? os : _os));
			}
		} catch (Exception e) {
			if (isIgnorableException(e)) {
				if (e.getMessage().contains("Broken pipe") || e.getMessage().contains("Connection reset")) {
					log.derror("__write_to_socket:nio_copyable: Failed to write! [error: " + e + "]");
				} else {
					log.error("__write_to_socket:nio_copyable: Failed to write! [" + e + "]");
				}
			} else {
				log.error("__write_to_socket:nio_copyable: Failed to write!", e);
			}
			IOUtils.closeAllQuietly(response, socket, out, _os, is);
			__keep_alive.set(false);
			return;
		}
		try {
			if (os != null && !shouldUseChunked) {
				__finish_stream(os);
			}
		} catch (Exception e) {
			if ((e instanceof SocketException && e.getMessage().contains("Broken pipe")) || e instanceof SSLException || e instanceof SocketTimeoutException
					|| (e instanceof SocketException && e.getMessage().contains("Connection reset"))
					|| (e instanceof IOException && e.getMessage().contains("closed")) || e instanceof ConnectException) {
				IOUtils.closeAllQuietly(response, socket, out, _os, is);
				__keep_alive.set(false);
			} else
				log.error("__finish_stream: Failed to flush stream!", e);
			return;
		}
		if (this.isTimingDebugEnabled) {
			log.debug("__write_to_socket::copy-write: " + ((System.currentTimeMillis() - start) + " ms"));
		}
	}

	private void __copyable(final boolean shouldUseChunked, final Response response, final OutputStream os, final OutputStream _os,
			final HttpCompressionMethod comp, final Socket socket, final variable<Boolean> __keep_alive, final HTTPPrintWriter out, final InputStream is,
			final long start) throws Exception {
		try {
			if (shouldUseChunked) {
				this.__do_chunked_copyable(response.getCopyable(), (os != null ? os : _os), comp);
			} else {
				response.getCopyable().writeTo(os != null ? os : _os);
			}
		} catch (Exception e) {
			if (isIgnorableException(e)) {
				if (e.getMessage().contains("Broken pipe") || e.getMessage().contains("Connection reset")) {
					log.derror("__write_to_socket:copyable: Failed to write! [error: " + e + "]");
				} else {
					log.error("__write_to_socket:copyable: Failed to write! [" + e + "]");
				}
			} else {
				log.error("__write_to_socket:copyable: Failed to write!", e);
			}
			IOUtils.closeAllQuietly(response, socket, out, _os, is);
			__keep_alive.set(false);
			return;
		}
		try {
			if (os != null && !shouldUseChunked) {
				__finish_stream(os);
			}
		} catch (Exception e) {
			if (isIgnorableException(e)) {
				IOUtils.closeAllQuietly(response, socket, out, _os, is);
				__keep_alive.set(false);
				return;
			} else
				log.error("__write_to_socket:copyable: __finish_stream() failed.", e);
		}
		if (this.isTimingDebugEnabled) {
			log.debug("__write_to_socket::copy-write: " + ((System.currentTimeMillis() - start) + " ms"));
		}
	}

	private void __legacy_copy(final boolean __has_chunks, final ByteReadRange[] byteRanges, final Response response, final OutputStream os,
			final OutputStream _os, final HttpCompressionMethod comp, final boolean shouldUseChunked, final variable<Boolean> __keep_alive, final Socket socket,
			final HTTPPrintWriter out, final InputStream is, final long start) {
		try {
			if (__has_chunks) {
				__do_byte_ranges(byteRanges, response.getB(), os != null ? os : _os);
				log.debug("__legacy_copy::byte-ranges: " + ((System.currentTimeMillis() - start) + " ms"));
			} else {
				if (this.flags.enable_chunked_transfers && shouldUseChunked) {
					__do_chunked_transfer(response.getB(), (os != null ? os : _os), comp);
					log.debug("__legacy_copy::chunked-transfer: " + ((System.currentTimeMillis() - start) + " ms"));
				} else {
					copy(ccm, response.getB(), _os);
					log.debug("__legacy_copy::copy: " + ((System.currentTimeMillis() - start) + " ms"));
				}
			}
		} catch (Exception e) {
			if (!isIgnorableException(e)) {
				log.error(e);
			}
			IOUtils.closeAllQuietly(response, socket, out, _os, is);
			__keep_alive.set(false);
			return;
		}
		if (this.isTimingDebugEnabled) {
			log.debug("__legacy_copy::uncomp-write: " + ((System.currentTimeMillis() - start) + " ms"));
		}
	}

	private void __fallback_copy(final boolean __has_chunks, final ByteReadRange[] byteRanges, final Response response, final OutputStream os,
			final boolean shouldUseChunked, final HttpCompressionMethod comp, final variable<Boolean> __keep_alive, final Socket socket,
			final HTTPPrintWriter out, final OutputStream _os, final InputStream is, final long start) throws Exception {
		if (__has_chunks) {
			__do_byte_ranges(byteRanges, response.getB(), os);
			if (this.isTimingDebugEnabled) {
				log.debug("__write_to_socket::fallback-write-range: " + ((System.currentTimeMillis() - start) + " ms"));
			}
		} else {
			if (shouldUseChunked) {
				__do_chunked_transfer(response.getB(), os, comp);
				if (this.isTimingDebugEnabled) {
					log.debug("__write_to_socket::fallback-write-chunked: " + ((System.currentTimeMillis() - start) + " ms"));
				}
			} else {
				copy(ccm, response.getInputStream(), os);
				if (this.isTimingDebugEnabled) {
					log.debug("__write_to_socket::fallback-write: " + ((System.currentTimeMillis() - start) + " ms"));
				}
			}
		}
		try {
			if (this.isTimingDebugEnabled) {
				log.debug("__write_to_socket::fallback-flush: " + ((System.currentTimeMillis() - start) + " ms"));
			}
			if (!shouldUseChunked) {
				__finish_stream(os);
			}
		} catch (Exception e) {
			if (isIgnorableException(e)) {
				IOUtils.closeAllQuietly(response, socket, out, _os, is);
				__keep_alive.set(false);
				return;
			} else {
				log.error("__finish_stream: Failed to flush stream!", e);
			}
		}
		if (this.isTimingDebugEnabled) {
			log.debug("__write_to_socket::fallback-write-close: " + ((System.currentTimeMillis() - start) + " ms"));
		}
	}

	private void __write_to_socket(final Socket socket, HttpCompressionMethod comp, final Map<String, String> headers, final HTTPPrintWriter out,
			final Response response, final InputStream is, final OutputStream _os, final Page page, final variable<Boolean> __keep_alive,
			final CopyAcceleration ccm, final long start, final boolean __enable_chunk_read, final RequestProtocolType protocol) throws Exception {
		final boolean shouldUseChunked = response.shouldPreferChunking() ? protocol.isHTTP()
				: (this.flags.enable_chunked_transfers && protocol.isHTTP()
						&& (this.flags.enable_compressed_chunked_transfers ? true : !response.copyable() && !response.nioCopyable()));
		if (this.flags.enable_chunked_transfers && __keep_alive.get() && shouldUseChunked) {
			out.println("Transfer-Encoding: chunked");
			out.println("Connection: keep-alive");
			out.println("Keep-Alive: timeout=" + KEEP_ALIVE_TIMEOUT + ", max=" + KEEP_ALIVE_MAX_CONNS);
		} else if (this.flags.enable_chunked_transfers && shouldUseChunked) {
			out.println("Connection: close");
			out.println("Transfer-Encoding: chunked");
			if (this.flags.enable_early_socket_close && protocol.isHTTP()) {
				IOUtils.closeInputQuietly(socket);
			}
		} else if (__keep_alive.get()) {
			out.println("Connection: keep-alive");
			out.println("Keep-Alive: timeout=" + KEEP_ALIVE_TIMEOUT + ", max=" + KEEP_ALIVE_MAX_CONNS);
		} else if (!__keep_alive.get()) {
			out.println("Connection: close");
			if (this.flags.enable_early_socket_close && protocol.isHTTP()) {
				IOUtils.closeInputQuietly(socket);
			}
		} else {
			Macros.unreachable();
			return;
		}
		if (this.isTimingDebugEnabled) {
			log.debug("__write_to_socket::post-flags: " + ((System.currentTimeMillis() - start) + " ms"));
		}
		if (isSocketClosed(socket, HTTPRequestPhase.DURING_RESPONSE)) {
			log.debug("__write_to_socket: Socket is closed, bailing out.");
			IOUtils.closeAllQuietly(is, out, socket, response);
			__keep_alive.set(false);
			return;
		} else {
			ByteReadRange[] byteRanges = null;
			if (__enable_chunk_read) {
				final String _range = headers.get("Range");
				final String[] _core_values = Splitters.splitString(_range, "=");
				final String unit = _core_values[0];
				if (!unit.equalsIgnoreCase("bytes")) {
					log.warn("Unknown range delivery type. [got=" + unit + "]");
				} else {
					final String[] ranges = Splitters.splitString(StringArrayUtils.addFrom(_core_values, 1, ""), ",");
					for (final String range : ranges) {
						final String[] rangeData = Splitters.splitString(range, "-");
						if (rangeData.length > 2) {
							log.warn("Received a larger than normal range. Skipping.");
							continue;
						}
						long min = -1;
						if (rangeData.length >= 1) {
							min = MathUtil.safeParseL(rangeData[0], -1);
						}
						long max = -1;
						if (rangeData.length == 2) {
							max = MathUtil.safeParseL(rangeData[1], -1);
						}
						if (byteRanges == null) {
							byteRanges = new ByteReadRange[]
								{ new ByteReadRange(min, max) };
						} else {
							byteRanges = ArrayUtils.add(byteRanges, new ByteReadRange(min, max));
						}
						log.debug("Reading range: " + new ByteReadRange(min, max));
					}
				}
			}
			final boolean __has_chunks = __enable_chunk_read && byteRanges != null && byteRanges.length > 0;
			if (__has_chunks) {
				out.println("Content-Length: " + byteRanges[0].length());
			}

			IOUtils.flushQuietly(_os);

			final OutputStream os = get_os(response, socket, out, _os, comp, shouldUseChunked);
			if (this.isTimingDebugEnabled) {
				log.debug("__write_to_socket::os: " + ((System.currentTimeMillis() - start) + " ms"));
			}

			if (this.isTimingDebugEnabled) {
				log.debug("__write_to_socket::os_flushed: " + ((System.currentTimeMillis() - start) + " ms"));
			}

			if (response.copyable()) {
				__copyable(shouldUseChunked, response, os, _os, comp, socket, __keep_alive, out, is, start);
			} else if (response.nioCopyable()) {
				__nio_copyable(shouldUseChunked, response, os, _os, comp, socket, __keep_alive, out, is, start);
			} else if (os == null) {
				__legacy_copy(__has_chunks, byteRanges, response, os, _os, comp, shouldUseChunked, __keep_alive, socket, out, is, start);
			} else {
				__fallback_copy(__has_chunks, byteRanges, response, os, shouldUseChunked, comp, __keep_alive, socket, out, _os, is, start);
			}

			IOUtils.flushQuietly(os != null ? os : _os);

			if (this.isTimingDebugEnabled) {
				log.debug("__write_to_socket::finish: " + ((System.currentTimeMillis() - start) + " ms"));
				log.debug("keepAlive: " + __keep_alive.get());
			}

			IOUtils.closeQuietly(os != null ? os : _os);
			IOUtils.closeQuietly(response);
		}
	}

	private static void copy(final CopyAcceleration cc, final InputStream in, final OutputStream os) throws IOException {
		switch (cc) {
			case J9_COPY:
				TransferToBackport.transferTo(in, os);
				break;
			case FAST_COPY:
				IOUtils.fastCopy(in, os);
				break;
			default:
				IOUtils.copy(in, os);
				break;
		}
	}

	/**
	 * Allows a page to be processed then returning a Page from a separate thread.
	 * 
	 * @since 3.0
	 * @param page - Page
	 */
	public void deferredAddPage(final Page page) {
		td.asyncSubmit(() -> {
			HttpServer3.this.addPage(page);
		});
	}

	/**
	 * Allows a page to be processed then returning a Page from a separate thread.
	 * 
	 * @since 3.0
	 * @param page - Returnable&lt;Page&gt;
	 */
	public void deferredAddPage(final Returnable<Page> page) {
		td.asyncSubmit(() -> {
			HttpServer3.this.addPage(page.run());
		});
	}

	@Override
	public boolean addPage(final Page page) {
		if (page == null) {
			return false;
		}
		if (page instanceof HttpPage) {
			if (this.flags.use_convert_function)
				return this.PAGES.put(((HttpPage) page).getID(), HttpPage2.convert((HttpPage) page)) == null;
			else
				return this.PAGES.put(((HttpPage) page).getID(), page) == null;
		} else if (page instanceof HttpPage2) {
			return this.PAGES.put(((HttpPage2) page).getID(), page) == null;
		} else if (page instanceof HttpPage3) {
			if (((HttpPage3) page).isCaching() && this.cache != null) {
				final CachedPage c = this.cache.cache(((HttpPage3) page).getID(), page);
				if (c != null) {
					return this.PAGES.put(c.getID(), page) == null;
				} else {
					return this.PAGES.put(((HttpPage3) page).getID(), page) == null;
				}
			} else {
				return this.PAGES.put(((HttpPage3) page).getID(), page) == null;
			}
		} else if (page instanceof IWebSocketPage && this.flags.enable_ws) {
			return this.PAGES.put(((IWebSocketPage) page).getID(), page) == null;
		} else if (page instanceof SSEPage && this.flags.enable_sse) {
			return this.PAGES.put(((SSEPage) page).getID(), page) == null;
		} else {
			log.warn("Unknown page type, ignored. [page: " + page + "]");
			return false;
		}
	}

	public void addFileString(final String name, final String file) throws Exception {
		Variables.requireNonNull(name, "name");
		Variables.requireNonNull(file, "file");
		if (!this.addPage(new HttpPage3("/" + name) {

			@Override
			public Response onRequest(Request request) {
				return new Response(200, createStream(file));
			}
		}.setCachingMethod(CachingMethod.SHORT_CACHE).setChecks(EnhancedSecurityChecks.DISALLOW_POST_DATA, EnhancedSecurityChecks.ONLY_ALLOW_BASIC_METHODS))) {
			log.error("[addFileString] Failed to add: " + name);
		}
		;
	}

	public void addFileStringWithMime(final String name, final String file) throws Exception {
		Variables.requireNonNull(name, "name");
		Variables.requireNonNull(file, "file");
		final MimeType mt = MimeType.fromFileString(name);
		if (!this.addPage(new HttpPage3(name) {

			@Override
			public Response onRequest(final Request request) {
				final Response r = new Response(200, createStream(file));
				if (mt != MimeType.UNKNOWN)
					r.setCustomMime(mt.getType());
				return r;
			}
		}.setCachingMethod(CachingMethod.SHORT_CACHE).setChecks(EnhancedSecurityChecks.DISALLOW_POST_DATA, EnhancedSecurityChecks.ONLY_ALLOW_BASIC_METHODS))) {
			log.error("[addFileStringWithMime] Failed to add: " + name);
		}
		;
	}

	public void addFileBytes(final String name, final byte[] data) throws Exception {
		Variables.requireNonNull(name, "name");
		Variables.requireNonNull(data, "data");
		if (!this.addPage(new HttpPage3("/" + name) {

			@Override
			public Response onRequest(final Request request) {
				return new Response(200, createStream(data));
			}
		}.setCachingMethod(CachingMethod.SHORT_CACHE).setChecks(EnhancedSecurityChecks.DISALLOW_POST_DATA, EnhancedSecurityChecks.ONLY_ALLOW_BASIC_METHODS))) {
			log.error("[addFileBytes] Failed to add: " + name);
		}
		;
	}

	public void addFileBytesWithMime(final String name, final byte[] data) throws Exception {
		Variables.requireNonNull(name, "name");
		Variables.requireNonNull(data, "data");
		final MimeType mt = MimeType.fromFileString(name);
		if (!this.addPage(new HttpPage3("/" + name) {

			@Override
			public Response onRequest(final Request request) {
				final Response r = new Response(200, createStream(data));
				if (mt != MimeType.UNKNOWN)
					r.setCustomMime(mt.getType());
				return r;
			}
		}.setCachingMethod(CachingMethod.SHORT_CACHE).setChecks(EnhancedSecurityChecks.DISALLOW_POST_DATA, EnhancedSecurityChecks.ONLY_ALLOW_BASIC_METHODS))) {
			log.error("[addFileBytesWithMime] Failed to add: " + name);
		}
		;
	}

	public void addDynamicAssetBytes(final String name, final Class<?> reference, final String assetName) {
		Variables.requireNonNull(name, "name");
		Variables.requireNonNull(assetName, "assetName");
		Variables.requireNonNull(reference, "reference");
		final MimeType mt = MimeType.fromFileString(name);
		if (!this.addPage(new HttpPage3(name) {

			@Override
			public Response onRequest(final Request request) {
				final Response r = new Response(200, FileUtil.getResourceStream(reference, assetName));
				if (mt != MimeType.UNKNOWN)
					r.setCustomMime(mt.getType());
				return r;
			}
		}.setCachingMethod(CachingMethod.SHORT_CACHE).setChecks(EnhancedSecurityChecks.DISALLOW_POST_DATA, EnhancedSecurityChecks.ONLY_ALLOW_BASIC_METHODS))) {
			log.error("[addDynamicAssetBytes] Failed to add: " + name);
		}
	}

	public void addDynamicAssetBytes(final String name, final Class<?> reference, final String assetName, final CachingMethod cache) {
		this.addDynamicAssetBytes(name, reference, assetName, cache, false);
	}

	public void addDynamicAssetBytes(final String name, final Class<?> reference, final String assetName, final CachingMethod cache,
			final boolean captchaExclusion) {
		Variables.requireNonNull(name, "name");
		Variables.requireNonNull(assetName, "assetName");
		Variables.requireNonNull(reference, "reference");
		Variables.requireNonNull(cache, "cache");
		final MimeType mt = MimeType.fromFileString(name);
		if (!this.addPage(new HttpPage3(name) {

			@Override
			public Response onRequest(final Request request) {
				final Response r = new Response(200, FileUtil.getResourceStream(reference, assetName));
				if (mt != MimeType.UNKNOWN)
					r.setCustomMime(mt.getType());
				return r;
			}
		}.setCachingMethod(cache).setCaptchaExclusion(captchaExclusion).setChecks(EnhancedSecurityChecks.DISALLOW_POST_DATA,
				EnhancedSecurityChecks.ONLY_ALLOW_BASIC_METHODS))) {
			log.error("[addDynamicAssetBytes] Failed to add: " + name);
		}
	}

	public void overrideFileString(final String name, final String file) throws Exception {
		Variables.requireNonNull(name, "name");
		Variables.requireNonNull(file, "file");
		final HttpPage3 page = new HttpPage3("/" + name) {

			@Override
			public Response onRequest(Request request) {
				return new Response(200, createStream(file));
			}
		}.setCachingMethod(CachingMethod.SHORT_CACHE).setChecks(EnhancedSecurityChecks.DISALLOW_POST_DATA, EnhancedSecurityChecks.ONLY_ALLOW_BASIC_METHODS);
		this.PAGES.put(page.getID(), page);
	}

	public HttpServer3Settings settings() {
		return this.settings;
	}

	public HttpServer3Flags flags() {
		return this.flags;
	}

	public Logger logger() {
		return log;
	}

	public static void main(String[] args) throws Exception {
		Arguments.setup(args);
		final HttpServer3 hs3 = new HttpServer3(9000, true, SSLBehaviour.DISABLED, null);
		hs3.isTimingDebugEnabled = true;
		log.debugEnabled.set(true);
		hs3.flags.enable_http2 = true;
		hs3.addPage(new CDNCGITracePage());
		hs3.addPage(new TCPDebugPage());
		hs3.addPage(new ASyncHttpPage3("/") {

			@SuppressWarnings("resource")
			@Override
			public void onFutureRequest(final Request request, final ControllableFutureTask<Response> r) {
				try {
					if (request.hasData()) {
						JVM.println("data: " + new String(IOUtils.toByteArray(request.getInputStream())));
					}
					r.complete(new Response(200, createStream("<html><body>" + "\n"
							+ "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vestibulum gravida congue risus, gravida pretium sapien hendrerit nec. Nullam mattis ipsum vitae purus consequat rhoncus. Fusce nisi arcu, scelerisque ac eros non, cursus rhoncus ipsum. Morbi pharetra commodo nisl non bibendum. Quisque ac magna odio. Aliquam vel mattis lacus. Nam ornare, tortor ut hendrerit placerat, turpis metus tempus est, id rhoncus massa velit a nibh. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia curae;\n"
							+ "\n"
							+ "Nulla egestas, odio eget tincidunt maximus, ante elit condimentum tellus, nec eleifend nunc massa ac nisl. Ut iaculis sodales vehicula. Nam ipsum sem, finibus eget bibendum at, dictum vel lorem. Integer blandit leo mauris, et vehicula leo rutrum a. Nam congue nisi lacus, nec commodo justo ornare sed. Pellentesque aliquet sem ut finibus interdum. Donec porttitor rhoncus lectus, vel iaculis enim posuere ut. Sed quis pretium ante, quis semper sapien. Fusce in est tellus. Quisque at libero ut sem efficitur rutrum. In hac habitasse platea dictumst. Praesent mollis aliquam erat a tincidunt.\n"
							+ "\n"
							+ "Mauris neque libero, lacinia at sodales nec, maximus a ligula. Sed sodales tellus ac eros posuere, at malesuada nulla venenatis. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Pellentesque pellentesque leo a sapien fringilla iaculis. Nunc molestie, augue posuere feugiat accumsan, enim libero semper sem, a finibus mi purus nec orci. Phasellus finibus consequat odio, id condimentum magna pulvinar sed. Sed a aliquam nunc. Etiam convallis sem in ante sagittis auctor. Nullam fringilla ante nisi, at volutpat arcu accumsan sed. Interdum et malesuada fames ac ante ipsum primis in faucibus.\n"
							+ "\n"
							+ "Nullam orci dolor, sollicitudin sit amet placerat at, iaculis non augue. Nam at laoreet velit. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Pellentesque vel metus eu ipsum placerat sagittis. Proin massa dui, lobortis ac nisl ut, lobortis gravida tellus. In non facilisis nibh. Vivamus sit amet luctus lacus, vel molestie urna. Etiam fringilla, enim nec lacinia cursus, velit lectus faucibus massa, sit amet egestas tortor lacus auctor massa. Morbi vitae venenatis nibh, at condimentum velit.\n"
							+ "\n"
							+ "Cras vulputate, sapien id varius faucibus, justo purus aliquam dui, sed hendrerit leo purus eget risus. Ut porttitor tristique tellus, ac blandit tellus. Nullam accumsan ornare aliquam. Maecenas tincidunt sem nec viverra facilisis. Nullam egestas lacus sit amet velit condimentum rhoncus. Sed ac ornare tortor. Phasellus tincidunt eu lorem vitae varius. Proin euismod rutrum facilisis. Aliquam eu feugiat neque. Fusce varius felis non magna feugiat vehicula sed quis arcu. Ut ac enim quam. Donec porta luctus mauris eget interdum. Nullam sit amet pellentesque enim, pharetra varius eros. Aliquam fringilla volutpat libero, vitae ornare purus condimentum vel. Integer a odio risus.</body></html>"))
							.header("X-Test", "Hello").setCustomMime("text/html"));
				} catch (Exception e) {
					r.complete(new Response(503));
				}
			}

		}.setCachingMethod(CachingMethod.VERY_LONG_CACHE));
		hs3.addPage(new ASyncHttpPage3("/proxy-demo") {

			@Override
			public void onFutureRequest(final Request request, final ControllableFutureTask<Response> r) {
				try {
					if (request.hasData()) {
						JVM.println("data: " + new String(IOUtils.toByteArray(request.getInputStream())));
					}
					final HttpResponse<InputStream> rx = HttpClient.getStream().url("http://motherfuckingwebsite.com/").create();
					r.complete(new Response(200, rx, rx));
				} catch (Exception e) {
					r.complete(new Response(503));
				}
			}

		}.setCachingMethod(CachingMethod.VERY_LONG_CACHE));
	}

	public MicroCache getCache() {
		return this.cache;
	}

	public void setDebugging(final boolean value) {
		this.isTimingDebugEnabled = value;
		log.debugEnabled.set(value);
		log.traceEnabled.set(value);
	}

	protected ExecutorService getExecutor() {
		return this.td.executor();
	}

	public void dumpPages() {
		ArrayUtils.printLln(this.PAGES);
	}
}
